[2] CSP object-src empty-source-list bypass for no-URL plugin elements
이 diff는 CSP object-src 정책의 bypass를 수정합니다. URL 없이 사용된 <object>/<embed> 인스턴스화에 대해 빈 source list가 deny-all이 아닌 허용으로 처리되던 문제였습니다. 이 버그는 배포된 정책에 의존하는 페이지의 plugin attack surface를 확대하지만, 직접적인 memory primitive는 발생하지 않습니다. Exploit을 위해서는 이미 HTML injection이 가능한 상태가 전제되어야 하며, 영향 범위는 'none' 대신 빈 source list 형태를 사용하는 CSP에 한정됩니다.
<object> 또는 <embed> 엘리먼트에 data/src 속성이 없는 경우, 기존 WebKit은 빈 URL을 CSP 검사에 전달했습니다. 이 경로에는 'none' 키워드가 명시된 경우에만 차단하는 특수 로직이 적용되어 있었습니다. 빈 source list(object-src;)는 CSP Level 3 §6.7.2.7에 따라 'none'과 동일하게 취급해야 하지만, 기존 코드는 이를 잘못 허용했습니다. 이번 패치는 §6.1.9 특수 케이스를 완전히 제거합니다. 대신 URL이 없는 엘리먼트에 대해서는 document 자체의 URL을 source-list 매칭의 fallback으로 사용합니다. Document URL은 빈 source list와 'none'에 대해 자연스럽게 매칭되지 않아 차단되고, 'self'나 wildcard에 대해서는 허용됩니다. 새로 추가된 WPT 테스트 3개는 URL 없이 사용된 <object>/<embed>가 object-src 'none'과 빈 source list 모두에서 차단되는지 검증합니다.
Source/WebCore/page/csp/ContentSecurityPolicySourceListDirective.cpp
Source/WebCore/page/csp/ContentSecurityPolicy.cpp
LayoutTests/imported/w3c/web-platform-tests/content-security-policy/object-src/object-src-no-url-empty-source-list-blocked.html
Patch Details
패치는 ContentSecurityPolicySourceListDirective::allows에서 ShouldAllowEmptyURLIfSourceListIsNotNone 파라미터를 제거합니다. 이제 이 함수는 빈 URL에 대해 무조건 false를 반환합니다. ContentSecurityPolicy::allowObjectFromSource에서는 전달된 url이 비어 있을 때(data/src가 없는 <object>/<embed>의 경우), 정책 매칭을 수행하기 전에 m_protectedURL(document 자체의 URL)로 대체합니다. 해당 파라미터는 checkSource(), violatedDirectiveForObjectSource(), 그리고 관련 allows() 오버로드 시그니처에서도 함께 제거되었습니다. checkFrameAncestors()의 두 오버로드에서도 제거된 세 번째 인자가 삭제되었습니다.
source-list 매칭의 empty-URL fast path에서 두 CSP 상태('none' 키워드와 빈 source list)를 혼용하여, 빈 source list가 deny-all이 아닌 허용으로 동작한 패턴.
Background
Content Security Policy는 HTTP 헤더 또는 meta 태그를 통해 전달되는 정책으로, document가 각 리소스 유형에 대해 허용할 source를 제한하는 데 사용됩니다. object-src 디렉티브는 <object>/<embed> plugin 로딩을 제어합니다. source list는 디렉티브의 오른쪽 값으로, 'self'·'none'·scheme/host 표현식 등의 토큰을 포함하거나 비어 있을 수 있습니다. CSP Level 3 명세에 따르면 빈 source list는 어떤 URL도 매칭하지 않으며, 'none'과 동일하게 동작합니다.
WebKit에서는 ContentSecurityPolicySourceList로 source list를 모델링합니다. isNone()은 리터럴 'none' 토큰에 대해서만 true를 반환하며, 빈 리스트에 대해서는 false를 반환합니다. <object>/<embed>는 URL 없이도 plugin을 인스턴스화할 수 있는데, data와 src 속성이 모두 없는 경우에는 type 속성만으로 plugin이 선택됩니다. 초기 CSP 초안에는 이러한 no-URL plugin에 대해 'none'이 명시되지 않은 한 허용하는 특수 조항이 있었으며, 이것이 이번 버그의 배경이 되었습니다.
Analysis
패치 이전 allows() 내부 조건은 shouldAllowEmptyURLIfSourceListEmpty == Yes && !m_sourceList.isNone()이었습니다. 이 조건의 목적은 §6.1.9 특수 케이스, 즉 URL 없는 plugin은 'none'이 명시된 경우에만 차단하고 그 외에는 허용한다는 원칙을 구현하는 것이었습니다. 그러나 이 조건은 두 가지 서로 다른 CSP 상태를 혼용합니다. (a) 명시적인 'none' 키워드와 (b) 빈 source list(object-src;)입니다.
CSP Level 3 §6.7.2.7에 따르면 이 둘은 의미상 동일합니다. 둘 다 어떤 URL도 매칭하지 않아야 합니다. 그러나 m_sourceList.isNone()은 리터럴 'none' 토큰에 대해서만 true를 반환합니다. 결과적으로 object-src; 정책에서는 Yes && !false = true가 되어, 배포된 정책이 모든 object source를 금지하는 의도임에도 no-URL <object>/<embed>가 기본 plugin을 로딩할 수 있었습니다.
Content-Security-Policy: object-src;로 보호된 페이지에서 HTML injection이 가능한 공격자가 있다면, data/src 없이 <object type="..."></object> 또는 <embed type="...">를 삽입할 수 있었습니다. 패치 이전에는 빈 URL 경로가 true를 반환하여 allowObjectFromSource()가 plugin 인스턴스화를 허용했습니다. type 속성으로 선택된 plugin은 deny-all 정책이 배포되어 있어도 실행될 수 있었습니다.
이번 패치의 접근 방식, 즉 리소스 URL이 비어 있을 때 document 자체의 URL로 대체하는 방식은 보다 명확한 패턴입니다. 별도의 특수 로직 없이 동일한 matcher가 no-URL 케이스를 처리할 수 있게 됩니다. Document URL은 'none'과 빈 source list에 대해 자연스럽게 매칭되지 않으며, 'self'와 wildcard에 대해서는 허용됩니다.
이 vulnerability는 CSP object-src defense-in-depth 경계를 약화시킵니다. HTML/CSP 명세는 object-src;와 object-src 'none'을 동일하게 취급하며, 둘 다 모든 plugin 인스턴스화를 금지해야 합니다. 패치 이전에는 no-URL plugin 경로에서 이 불변 조건이 위반되었습니다. object-src;로 보호된 document에 <embed type="...">나 <object type="..."></object>를 삽입하면, 브라우저가 해당 type의 기본 plugin을 인스턴스화하도록 유도할 수 있었습니다. 이미 HTML injection이 가능한 공격자라면 배포된 CSP에도 불구하고 페이지의 plugin attack surface를 확대하는 것이 이론적으로 가능했습니다. 이는 object/embed 기반 exploit을 무력화하기 위해 배포된 mitigation을 약화시키는 취약점이었습니다.
Audit directions
-
"no associated URL" 또는 "opaque origin" 입력에 대한 legacy spec 특수 케이스를 포함하는 CSP/security-policy matcher.
violatedDirectiveForFrame,violatedDirectiveForMedia,violatedDirectiveForConnectSource등 다른 source-list 소비자와ContentSecurityPolicySourceList::matches경로를 점검합니다.url.isEmpty(),protocolIsAbout(),isOpaque()를 분기 조건으로 사용하는 코드 중 명시적'none'토큰과 빈 source list 사이에서 동작이 달라지는 부분을 확인합니다.Source/WebCore/page/csp/ContentSecurityPolicySourceList.cpp에서 시작하여isNone()호출 지점을 검색합니다. -
flag == Yes && !state.isX()형태의 조건식 — "X 상태가 설정됨"과 "X 상태가 trivial하지 않음"을 혼용하는 패턴. CSP 및 기타 정책 코드에서 부정 연산자가 의미상 서로 다른 여러 케이스를 포괄하는 enum/flag의false값에 적용되는 유사한 2중 조건 게이트를 검토합니다 (예:'none'리터럴 vs. 빈 리스트, default vs. unset). 보안 결정을 담당하는 boolean 조건 내에서!.*isNone()과!.*isEmpty()를 WebCore 전체에서 검색합니다. -
ContentSecurityPolicy.h의allowObjectFromSource및 기타allowXFromSource진입점 호출 지점. 빈URL을 "리소스 없음"의 의미로 합성하는 각 호출 지점에서, document URL fallback 적용 이후 올바른 결정이 내려지는지(또는 빈 URL이 즉시 거부되는지) 확인합니다.HTMLPlugInElement::allowedToLoadFrameURL과LoadedFromPluginElement::Yes로 플래그된CachedResourceLoader::canRequest경로에서 시작합니다. -
meta 태그 방식과 HTTP 헤더 방식 CSP 간의 동작 일관성. 새로 추가된 WPT 케이스를 두 가지 전달 방식 모두로 실행합니다. 요청 URL이 없거나 about:blank인 경우,
frame-src,media-src,connect-src가'none'과 빈 source list에 대해 동일한 차단 결정을 내리는지 확인합니다.
Note: 일부 내용(
m_sourceList.isNone()이 리터럴'none'토큰에 대해서만 true를 반환한다는 점,allowObjectFromSource의 특정 호출 지점, §6.7.2.7 명세 인용)은 diff에 직접 드러나지 않으며, patch 주석과 주변 코드로부터 추론된 것입니다. 다만 bypass 메커니즘, 즉 빈 source list가 허용 분기에 도달하는 경로는 패치가 추가한 테스트 케이스를 통해 직접 확인됩니다.