[5] Unbounded coded dimensions in WebCodecs encoder configuration
WebCodecs accepted a 4-billion-pixel frame width and passed it along.
Medium — validation 누락 자체는 명확하고 script로 도달 가능합니다. 다만 memory safety 측면의 결과는 WebKit tree 바깥의 third-party encoder 코드 안에서 발생합니다. commit title은 WebRTC VP9 encoder의 heap buffer overflow를 원인으로 지목합니다. 실제로 입증된 write가 아니라 이 출처 표기가 severity의 상한을 결정합니다.
attacker가 지정한 크기 값에 상한이 걸려 있지 않으면, 그 값이 전혀 예상하지 못한 코드의 allocation이나 geometry 계산까지 도달합니다. WebCodecs는 encode와 decode 제어를 JavaScript에 그대로 노출하는 저수준 web API입니다. 이 중 encoder 쪽은 codec 문자열과 bitrate, coded frame의 가로·세로 크기를 담은 VideoEncoderConfig dictionary를 script로부터 직접 받습니다. WebCore는 이 설정을 검증한 뒤 내부 표현인 VideoEncoder::Config로 옮기고, platform encoder backend로 전달합니다. 결국 script가 고른 숫자와 third-party encoder 코드 사이를 가로막는 경계는 WebCore의 validation뿐입니다.
관전 포인트: 임의의 페이지가 WebCodecs video encoder의 coded dimension을 2^32-1까지 설정할 수 있고, WebCore 어디에서도 상한이 걸리지 않은 채 그 값이 platform encoder까지 도달합니다.
입력 범위를 결정하는 IDL은 다음과 같습니다:
Source/WebCore/Modules/webcodecs/WebCodecsVideoEncoderConfig.idl
[EnforceRange] unsigned long width;
[EnforceRange] unsigned long height;
그리고 이 값들이 복사되어 들어가는 타입입니다:
Source/WebCore/platform/VideoEncoder.h
uint64_t width;
uint64_t height;
Patch Details
isSupportedEncoderCodec의 시그니처가 (const String& codec, const SettingsValues&)에서 (const WebCodecsVideoEncoderConfig&, const SettingsValues&)로 변경되었습니다. 그리고 함수 본문 맨 앞에 조기 반환 경로가 추가되었습니다. constexpr size_t maxFrameDimension = 32767;을 선언한 뒤, 가로·세로 중 하나라도 이 값을 넘으면 codec 문자열 비교에 들어가기 전에 거부합니다. 호출 지점인 인스턴스 메서드 configure()와 static 메서드 isConfigSupported() 양쪽 모두 이제 codec 문자열만이 아니라 config 전체를 전달합니다. 함께 추가된 regression test는 경계를 정확히 고정합니다. 32767은 허용되고 32768은 거부됩니다.
이 check가 어디에 놓였는지는 우연한 선택이 아니라 동작에 직접 영향을 줍니다. 상한 검사가 isValidEncoderConfig가 아니라 codec 지원 여부를 판정하는 쪽에 들어갔기 때문입니다. 그래서 크기가 과도한 설정은 configure()에서 동기적으로 예외를 던지지 않습니다. 대신 encoder의 error callback을 통해 드러나며, regression test가 확인하는 경로도 이쪽입니다.
attacker가 지정한 크기 값에 대해 하한만 검증하고, 상한은 third-party 코드에서 그 값을 받아 처리하는 쪽에 암묵적으로 맡긴 패턴.
Background
이 코드가 있는 위치. Source/WebCore/Modules/webcodecs/WebCodecsVideoEncoder.cpp는 WebCodecs encoding API의 설정과 control message를 담당하는 WebCore 쪽 계층입니다. USE(LIBWEBRTC) 환경에서 VP8, VP9, AV1을 처리할 때 설정이 전달되는 backend는 LibWebRTCVPXVideoEncoder입니다. 이 클래스는 third-party인 libwebrtc와 libvpx encoder를 감싸는 얇은 WebCore wrapper에 해당합니다.
Web IDL의 [EnforceRange]. 이 extended attribute를 붙이면 선언된 타입의 범위를 벗어난 값이 조용히 wrap되지 않고, bindings 계층에서 TypeError가 발생합니다. unsigned long에 적용된 경우 bindings가 보장하는 범위는 [0, 2^32-1]입니다. 그보다 좁은 범위는 전혀 보장하지 않습니다. 어디까지나 타입 범위를 강제하는 장치일 뿐, 애플리케이션 수준의 상한은 아닙니다. 다만 크기 필드에 붙은 [EnforceRange]를 본 사람은 값이 이미 검증되었다고 오해하기 쉽습니다.
패치 이전의 validation 흐름. configure는 먼저 isValidEncoderConfig를 호출했고, 여기서는 width와 height가 0인 경우만 거부했습니다. 이어서 isSupportedEncoderCodec(config.codec, ...)을 호출했는데, 이 함수 본문은 처음부터 끝까지 codec 문자열만 확인했습니다. 이후 createVideoEncoderConfig가 크기 값을 그대로 VideoEncoder::Config에 복사했고, VideoEncoder::create가 그 설정을 backend로 전달했습니다.
Analysis
여기서 빠져 있던 invariant는 WebCodecs 설정 validation을 통과한 coded frame 크기는 platform encoder가 표현하고 할당할 수 있는 범위 안에 있어야 한다는 것입니다. WebCore가 강제한 조건은 "0이 아닐 것" 하나뿐이었습니다. 상한은 전적으로 backend에 맡겨져 있었습니다.
Before: After:
script config {w,h} script config {w,h}
└─► [EnforceRange] u32 ─ 0..2^32-1 └─► [EnforceRange] u32
└─► isValidEncoderConfig └─► isValidEncoderConfig
(rejects zero only) (rejects zero)
└─► isSupportedEncoderCodec └─► isSupportedEncoderCodec(config)
(codec string only) ├─ bail if w|h > 32767
└─► VideoEncoder::Config └─ then codec-string match
(uint64_t w/h) └─► VideoEncoder::Config
└─► platform backend (bounded)
└─► platform backend
validation 누락과 그 수정 자체는 단순명료합니다. 패치 이전에는 attacker가 고른 2^32-1까지의 크기 값이 JavaScript에서 출발해 0 검사를 통과하고, codec 문자열만 보는 지원 여부 판정을 지나 VideoEncoder::Config에 들어간 뒤 backend까지 전달되었습니다. 그 경로 어디에도 상한을 확인하는 코드는 없었습니다.
하위 단계에서 실제로 무너지는 형태는 코드에서 직접 읽어낸 것이 아니라 추론한 결과입니다. 근거는 선택된 상수 값입니다. 패치가 고른 값은 32767, 즉 INT16_MAX입니다. UINT16_MAX도 아니고 면적 기준의 2의 거듭제곱 한계도 아닙니다. regression test 역시 32767은 허용하고 32768은 거부하도록 못박아 두었습니다. 이 선택은 면적이나 바이트 수 기준의 상한이 아니라, WebCore 아래 어딘가에 signed 16-bit 표현 한계가 있음을 가리킵니다. 이 해석을 따르면, INT16_MAX를 넘는 크기 값은 encoder 내부 geometry 필드에 저장되는 시점에 narrowing됩니다. 그 결과 narrowing된 값을 기준으로 크기가 잡힌 buffer는, 이후 encode() 호출로 제출되는 frame의 실제 geometry와 어긋나게 될 가능성이 있습니다.
commit title은 이 결과를 "Heap Buffer Overflow in WebRTC VP9 Encoder"로 지목합니다. 다만 경계에서 WebKit이 담당하는 범위는 LibWebRTCVPXVideoEncoder까지입니다. allocation이 일어나는 지점과 out-of-bounds write는 libwebrtc/libvpx 안에 있습니다. 즉 overflow는 이번 변경이 직접 보여주는 사실이 아니라 commit title의 출처 표기에 해당합니다.
script가 넘기는 media 파라미터와 WebCore가 품고 있는 third-party codec 코드 사이의 경계가 약해진 셈입니다. 게다가 이 경로는 user gesture 없이 임의의 페이지에서 도달할 수 있는 지점입니다.
Audit directions
- script가 넘긴 크기 값에 하한만 검사하는 validation.
isValid*Config라는 이름을 달고 있으면서 0만 거부하는 함수는, 완전한 검증처럼 보이는 부분 검증에 지나지 않습니다. 점검 대상은 WebCodecs의 나머지 표면입니다. decoder 설정,VideoFrame생성,ImageDecoder를 대상으로, 값이 platform backend에 도달하기 전에 상한 검사가 존재하는지 확인할 필요가 있습니다. 눈에 띄는 신호는[EnforceRange] unsigned long으로 선언된 IDL 필드인데 WebCore 쪽 검사가!value하나뿐인 경우입니다. - 타입의 자연스러운 최대값 대신
INT16_MAX가 상한으로 선택된 경우. 패치가 65535 대신 32767을 고른다면, 하위 어딘가의 signed narrowing에 대한 지식이 그 상수에 담겨 있다고 볼 수 있습니다. WebCore가 third-party media 라이브러리로 크기 값을 전달하는 지점이라면, WebCore 쪽 타입(여기서는uint64_t)이 라이브러리 내부 표현보다 넓은지 확인해 볼 필요가 있습니다. 둘 사이의 폭 차이가 곧 narrowing 버그가 자리 잡는 공간이기 때문입니다.
Note: signed 16-bit narrowing은 상한으로 INT16_MAX가 선택되었다는 점과 test의 32767/32768 경계에서 추정한 방향입니다. 실제로 narrowing이 일어나는 필드는 libwebrtc/libvpx 안에 있으며, 여기서 특정되지는 않습니다.