요약

  • WebSocket 마스킹은 기밀성이 아닙니다. 4바이트 키는 프레임 안에 함께 전송되며, 그 예측 불가능성은 브라우저 애플리케이션이 프레임의 전송상 바이트 이미지를 미리 선정하지 못하게 하는 역할을 합니다.
  • 이 규칙은 구체적인 중계 장비 장애에서 비롯되었습니다. 클라이언트 프레임은 새로운 키로 마스킹해야 하고, 서버 프레임은 마스킹해서는 안 되며, 애플리케이션은 전송이 시작된 뒤 프레임을 다시 쓸 수 없습니다.

초기 프레임은 그 경계를 신뢰했습니다

IETF의2010년 5월 WebSocket 초안은 HTTP 개시 교환 이후 브라우저에 양방향 채널을 제공했습니다. 초기 전송 형식은 의도적으로 작았습니다. 텍스트 프레임은0x00으로 시작해0xff로 끝났고, 바이너리 형태의 프레임은 길이를 담았습니다. 핸드셰이크가 성공하면 양쪽이 독립적으로 메시지를 보낼 수 있었습니다.

규격을 준수하는 WebSocket 엔드포인트에게 이 구분자들은 프레임을 식별하기에 충분했습니다. 그러나 이미 경로 위에 놓여 있던 모든 장비를 다루기에는 충분하지 않았습니다. 가로채기 프록시는 HTTP 업그레이드를 받아들인 뒤에도, 새 스트림 안에서 또 다른 HTTP 요청이 시작될 수 있다고 가정하며 이후 바이트를 계속 훑을 수 있었습니다. WebSocket 문법은 바뀌었지만 중계 장비의 파서는 바뀌지 않았습니다.

이것은 기묘한 보안 경계였습니다. 브라우저는 웹 플랫폼이 원하던 일, 즉 동의한 서버와 사이트가 통신하도록 하는 일을 정확히 수행하고 있었습니다. 서버도 핸드셰이크가 허용하는 일을 정확히 하고 있었습니다. 가운데 있는 위험한 행위자는 악의적일 필요조차 없었습니다. 다음 바이트의 소유 프로토콜이 무엇인지에 대해 자신 있게 틀리기만 하면 되었습니다.

고정된 위장은 통제권을 없애지 않았습니다

2011년 1월의-04초안은 클라이언트가 서버로 보내는 모든 프레임을 마스킹하도록 요구했습니다. 그 마스킹 키는 핸드셰이크에서 교환된 값에서 나왔고, 연결 내내 일정했습니다.

이 설계는 눈에 보이는 페이로드를 바꿨지만, 방어에 필요한 성질은 아직 확립하지 못했습니다. 변환이 안정적이고 알려져 있다면, 애플리케이션은 원하는 출력을 만드는 입력을 선택할 수 있습니다. 그 바이트들은 규칙을 학습하지 않은 쪽에게만 뒤섞여 보입니다. 규칙이 예측 가능해지는 순간, 위장은 통제권을 발신자에게 돌려줍니다.

이 약점은 개념적인 것이었으며, 특정 제품에 대한 판정이 아니었습니다. 연결 전체에 걸친 키는 마스킹을 스트림에 입히는 장식으로 다뤘습니다. 실제 위협에는 프레임마다 새로운 약속이 필요했습니다. 애플리케이션은 논리적 메시지를 선택하거나, 다음 변환을 알 수 있거나, 둘 중 하나만 가능해야 했고, 전송 바이트가 확정되기 전에 둘 다 할 수 있어서는 안 되었습니다.

한 프록시가 하나의 응답에 잘못된 이름을 붙였습니다

2011년 논문Talking to Yourself for Fun and Profit은 투명, 더 정확히는 가로채기 프록시가 있는 환경에서 브라우저 소켓 메커니즘을 검토했습니다. 그런 장비 가운데 일부는 상태 전이를 이해하지 못한 채 동의 또는 업그레이드 트래픽을 전달했습니다. 그리고 이후 스트림에서 공격자가 고른 바이트를 HTTP 요청으로 해석했습니다.

이 장애는 여러 소유자에 걸쳐 있었습니다. 악성 사이트가 브라우저를 공격자가 통제하는 서버로 연결하도록 유도했습니다. 혼란에 빠진 중계 장비가 이후 바이트를 다른 리소스 식별자를 지닌 요청으로 해석했습니다. 공격 서버가 응답처럼 보이는 바이트를 공급했습니다. 중계 장비는 그 응답을 거짓 식별자 아래 저장했고, 같은 캐시의 후속 사용자들은 명명된 오리진에서 온 것이 아닌 콘텐츠를 받게 되었습니다.

연구자들은 가상의 파서를 만들어 낸 데 그치지 않았습니다. 2011년 3월의 광고 조사는 측정된 Java·Flash 경로의 작지만 0이 아닌 비율에서 캐시 오염 조건을 발견했고, 테스트에 도달한 47,338건의 업그레이드 기반 WebSocket 스트로우맨 핸드셰이크 가운데 8건의 성공 사례를 기록했습니다. 이 수치는 그 실험과 그 시대에 속합니다. 오늘날 프록시의 인구조사가 아닙니다.

논문의 지속적 기여는 보급률 결과보다 좁았습니다. 비 HTTP 접두어를 추가하는 것은 어떤 망가진 중계 장비에 대해서도 안전을 증명할 수 없다는 주장이었습니다. 알려지지 않은 파서는 접두어를 건너뛰고 페이로드에서 다시 파싱을 재개할 수 있습니다. 더 안전한 불변식은 적대적 애플리케이션 코드가 페이로드의 전송상 표현을 통제하지 못하게 하는 것이었습니다.

키가 모든 프레임 안으로 들어왔습니다

2011년 2월의-05초안이 결정적 변경을 만들었습니다. 각 클라이언트 프레임이 자신만의 32비트 키를 실었습니다. 클라이언트는 그 키를 강한 엔트로피원에서 골라야 했고, 다음 키는 이전 키들로부터 예측 가능해서는 안 되었습니다.RFC 4086은 더 넓은 경고를 공급합니다. 통계적으로 불규칙해 보이는 출력조차 적에게는 예측 가능할 수 있으며, 특히 시계, 카운터 또는 작은 시드 공간에서 나올 때 그렇습니다.

최종RFC 6455는 프레임별 설계를 유지했습니다. 프레임의 MASK 비트는 4개의 키 옥텟이 존재하는지를 선언합니다. 페이로드 옥텟i는 키 옥텟i mod 4와의 XOR로 변환됩니다. 이 연산은 페이로드 길이를 바꾸지 않으며, 키 자체는 그 길이에 포함되지 않습니다.

프레임을 받는 누구든 XOR를 되돌릴 수 있습니다. 그것은 의도된 것입니다. 애플리케이션은 예측 불가능한 키를 알기 전에 페이로드를 확정해야 했고, 서버는 페이로드를 복원하기 위해 키를 받아야 했습니다. 비밀 유지는 전혀 다른 주장을 지지했을 것입니다. 여기서 중대한 사실은 누가 전송 전에 바이트를 선택할 수 있는가였습니다.

전송이 시작되면 프레임은 불변이 되었습니다

프레임별 신선함은, 애플리케이션이 긴 프레임의 시작부에서 키를 알아내고 아직 보내지 않은 부분을 다시 쓸 수 있다면 실패합니다. RFC 6455는 이 공격을 정확히 기술합니다. 애플리케이션은 알려진 바이트로 시작해 전송된 접두어에서 반복되는 4바이트 변환을 추론한 뒤, 보내지 않은 접미어를 HTTP 요청처럼 보이는 마스킹된 형태로 바꿀 수 있습니다.

프로토콜은 따라서 시간적 경계를 긋습니다. 클라이언트 프레임의 전송이 시작되면, 그 프레임의 애플리케이션이 공급한 페이로드는 더 이상 애플리케이션이 수정할 수 없어야 합니다. 바뀌거나 추가되는 데이터는 다른 프레임에 담기며, 그 프레임은 또 하나의 새 키를 받습니다.

이것은 “무작위성을 사용하라”는 말보다 강합니다. 무작위성을 되돌릴 수 없는 로컬 행위에 결합합니다. 프레임이 시작되기 전에는 애플리케이션이 콘텐츠 선택권을 갖습니다. 시작된 뒤에는 클라이언트 구현이 확정된 바이트 시퀀스를 소유합니다. 그 넘겨주기가 없다면, 애플리케이션은 접두어의 관찰을 꼬리에 대한 통제로 바꿀 수 있습니다.

방향이 위협 모델을 표현했습니다

RFC 6455는 모든 클라이언트→서버 프레임의 마스킹을 요구하고, 서버가 클라이언트로 보내는 프레임을 마스킹하는 것을 금지합니다. 서버는 클라이언트 프레임이 마스킹되지 않았을 때 연결을 닫아야 하고, 클라이언트는 서버 프레임이 마스킹되었을 때 닫아야 합니다. 프로토콜 오류 코드 1002가 이 실패를 기술할 수 있습니다.

이 비대칭은 의도된 것입니다. 캐시 오염 연쇄는 브라우저에서 서버 쪽으로 향하는 요청처럼 보이는 바이트를 필요로 했습니다. 악성 서버는 이미 응답처럼 보이는 데이터를 골라 낼 수 있지만, 위조된 요청이 없다면 혼란에 빠진 캐시는 이 공격을 완성하는 거짓 리소스 식별자를 얻지 못합니다. 반대 방향의 마스킹은 그 빠진 단계를 막지 못한 채 비용만 늘립니다.

이것은 서버 트래픽이 안전하다고 선언하는 것이 아닙니다. 서버→클라이언트 마스킹이 이 특정 인프라 공격에 대한 통제 수단이 아니라는 것만을 말합니다. 인증, 권한 부여, 오리진 정책, 콘텐츠 검증, 채널 보호는 각자의 역할을 유지합니다.

TLS는 이 규칙을 불필요하게 만들지 않았습니다

클라이언트 마스킹은 WebSocket이ws로 실행되든 TLS 안에서wss로 실행되든 적용됩니다. 암호화 아래에서는 경로 장비가 올바르게 보호된 TLS 스트림을 읽을 수 없으므로 이 규칙은 낭비처럼 보일 수 있습니다. 그러나 두 계약은 다릅니다. TLS는 그 엔드포인트들 사이의 기밀성과 무결성을 보호합니다. 마스킹은 신뢰할 수 없는 브라우저 코드가 규격을 준수하는 WebSocket 클라이언트로 하여금 어떤 프레임 페이로드 바이트를 배출하게 만들 수 있는지를 제한합니다.

둘 다 유지하는 것은 프레임의 유효성이 전송 계층이 어떻게 수립되었는지, TLS가 어디서 끝나는지에 의존하게 되는 일도 피합니다. 게이트웨이는 또 다른 내부 홉 전에 TLS를 끝낼 수 있습니다. 진단 경로는 바뀔 수 있습니다. 비브라우저 클라이언트도 같은 방향 비트를 지켜야 합니다. 한 계층의 보호가 다른 계층에 자신의 문법을 버릴 허가를 몰래 부여해서는 안 됩니다.

새로운 HTTP 전송 계층도 낡은 의무를 유지했습니다

RFC 8441은 이후 HTTP/2 스트림 위의 Extended CONNECT로 WebSocket을 부트스트랩했습니다. HTTP/2의:protocol메커니즘이 전이 신호를 공급하므로, 이 수립 경로에서 HTTP/1.1의Sec-WebSocket-Key와Sec-WebSocket-Accept처리를 제거했습니다. 그러나 프레임 마스킹을 폐지하지는 않았습니다. 이 문서는 RFC 6455의 보안 고려사항이 SHA-1에 관한 핸드셰이크별 논의를 제외하고 계속 적용된다고 말합니다.

RFC 9220은 Extended CONNECT 방식을 HTTP/3로 가져왔고 새로운 보안 예외를 도입하지 않았습니다. 부트스트랩은 연결 전체에 걸친 HTTP/1.1 스위치에서 선택된 멀티플렉스 스트림으로, 다시 QUIC 스트림으로 바뀌었습니다. 그러나 WebSocket 프레임은 같은 방향성 증거를 계속 실었습니다.

이 연속성은 시사적입니다. 마스킹은Upgrade헤더의 철자 하나에 대한 임시방편이 아니었습니다. 애플리케이션 선택, 클라이언트의 확정, 중계 장비의 혼란 사이의 관계를 encode한 것이며, 그 관계는 주변 전송 계층의 변화를 넘어 살아남았습니다.

공개된 키가 증명하는 것—그리고 증명하지 못하는 것

새로운 마스크 키는 신원을 증명하지 않습니다. 누가 메시지를 썼는지, 오리진이 허용 가능했는지, 서버가 행동을 승인했는지, 데이터가 보호된 전송 밖에서 온전히 유지되었는지를 보여주지 않습니다. 키의 재사용은 깨진 안전 가정의 증거이지만, 캡처에서 키가 보이는 것은 정상입니다.

또한 규격 준수 마스킹이 모든 중계 장비를 수리하지는 않습니다. RFC 6455는 비준수 클라이언트와 서버가 여전히 취약한 프록시를 같은 종류의 공격에 노출할 수 있음을 밝힙니다. 이 프로토콜은 규격을 준수하는 브라우저 경로가 만들어질 수 있는 일을 좁혔을 뿐, 인터넷의 모든 캐시를 장악하지는 않았습니다.

따라서 이 설계의 역사적 교훈은 절제되지만 지속적입니다. 낡은 인프라가 잘못된 문법으로 바이트를 파싱할 수 있는 곳에서, 새 프로토콜은 자신의 올바름을 선언하는 것만으로는 부족합니다. 신뢰할 수 없는 참여자가 공유 경로 위에 의도적으로 놓을 수 있는 위험한 바이트 패턴을 제한해야 할 수도 있습니다.

출처와 근거의 한계

초안과 RFC 기록은 설계 진화와 규범적 동작을 확립합니다. 논문은 2011년의 한정된 실험을 보고하며, 현재의 취약점이나 시장 점유율이 아닙니다. 어떤 출처도 특정 현대 브라우저, 프록시, CDN 또는 게이트웨이의 동작을 확립하지 않습니다. 마스킹은 암호화, 무결성 보호, 엔드포인트 인증도, 캐시된 응답이 겉보기 오리진에 속한다는 증거도 아닙니다.