[2] CSP empty object-src source list treated as permissive for URL-less plugins
The object-src spelling that looks like deny-all was the permissive one.
Medium으로 평가된 이유는, 스펙상 동일하게 정의된 두 정책이 서로 다른 결과를 냈고, 하필 허용 쪽으로 흐른 정책이 겉보기에는 deny-all처럼 보이는 표기였기 때문입니다. 메모리 primitive는 없습니다. 다만 플러그인 인스턴스화를 막으려고 배포한 CSP가 실제로는 그 역할을 못했다는 점이 문제입니다.
Content Security Policy는 문서가 리소스 타입별로 어떤 출처를 로드할 수 있는지 선언할 수 있게 해주며, object-src directive는 <object>와 <embed> 플러그인 로드를 통제하는 게이트 역할을 합니다. Directive의 우변은 source list로, 'self'나 'none' 같은 토큰, scheme/host 표현식, 혹은 아무것도 없는 형태를 가질 수 있습니다. CSP Level 3는 빈 source list가 어떤 URL과도 매치되지 않는다고 규정합니다. 즉 object-src;는 의미상 object-src 'none'과 동일하며, 둘 다 모든 플러그인 인스턴스화를 금지해야 합니다.
관전 포인트: object-src;로 강화된 페이지에서, HTML injection sink를 가진 공격자가 data/src 없는 <object type="...">나 <embed type="...">를 주입하면, 여전히 해당 type의 기본 플러그인이 인스턴스화됩니다.
<object>나<embed>element에data/srcattribute가 없을 때, WebKit은 이전까지 빈 URL을 CSP check에 전달하면서 리터럴'none'키워드에 대해서만 차단하는 special-case 로직을 사용했습니다. 빈 source list(object-src;)는 CSP Level 3 §6.7.2.7 기준으로'none'과 동등함에도 불구하고 잘못 허용되고 있었습니다.§6.1.9의 special-case 처리를 완전히 제거합니다. 대신 element에 연관된 URL이 없을 때는 문서 자체의 URL을 source list 매칭의 fallback으로 사용합니다. 문서 URL은 빈 source list나
'none'과는 자연스럽게 매치되지 않아 차단되고,'self'나 wildcard와는 매치되어 허용됩니다.
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
이번 변경으로 source-list 매칭에서 ShouldAllowEmptyURLIfSourceListIsNotNone special case가 제거되었고, 대신 호출자 쪽에서 URL을 치환하는 방식으로 대체되었습니다. ContentSecurityPolicySourceListDirective::allows()는 이제 URL이 비어 있을 때 !m_sourceList.isNone()을 참조하는 대신 무조건 false를 반환합니다. 이 파라미터는 함수 시그니처, checkSource(), violatedDirectiveForObjectSource(), 관련 allows() 오버로드에서 모두 사라졌고, 두 개의 checkFrameAncestors() 오버로드에서도 제거된 세 번째 인자가 함께 빠졌습니다. ContentSecurityPolicy::allowObjectFromSource()에서는 들어온 url이 비어 있는 경우 — 즉 data/src가 없는 <object>/<embed>의 경우 — 문서 자체의 m_protectedURL로 치환되며, 이 치환된 URL이 scheme bypass check, 보고되는 blocked URL, 그리고 allPoliciesAllow에 전달되는 URL로 일괄 사용됩니다. 새로 추가된 3개의 WPT 테스트는 URL이 없는 <object>/<embed>가 object-src 'none'과 object-src; 양쪽 모두에서 차단되는지 검증합니다.
Source-list 매칭의 빈-URL fast path에서 두 가지 CSP 상태('none' 키워드 vs. 빈 source list)를 혼동한 결과, 빈 source list가 deny-all이 아니라 허용 쪽으로 동작하게 된 패턴.
Background
CSP가 통제하는 대상.
Content Security Policy는 HTTP 헤더나 <meta http-equiv>로 전달되는 정책으로, 문서가 리소스 타입별로 로드 가능한 출처를 제한할 수 있게 해줍니다. object-src는 <object>/<embed> 플러그인 로드를 관장하며, allowObjectFromSource()가 WebKit에서 이 로드를 통제하는 게이트입니다. 이 함수는 HTMLPlugInElement, CachedResourceLoader, PolicyChecker에서 호출됩니다.
Source list와 두 가지 "빈" 표기.
Source list는 directive의 우변에 해당합니다. 'self', 'none' 같은 토큰이나 scheme/host 표현식을 가질 수도 있고, 비어 있을 수도 있습니다. CSP Level 3는 빈 source list가 어떤 URL과도 매치되지 않아 'none'과 동일하게 동작해야 한다고 규정합니다. WebKit은 source list를 ContentSecurityPolicySourceList로 모델링하며, isNone()은 리터럴 'none' 토큰일 때만 true를 반환하고 빈 list에 대해서는 false를 반환합니다.
연관 URL이 없는 플러그인 element.
<object>와 <embed>는 연관된 URL 없이도 플러그인을 인스턴스화할 수 있습니다. data도 src도 없는 경우, type attribute만으로 플러그인이 선택됩니다. 이 케이스 때문에 초기 CSP draft에는 §6.1.9라는 special clause가 있었는데, 'none'이 명시되지 않는 한 URL 없는 이런 플러그인을 허용하는 조항이었습니다. 하지만 더 간결해진 Level 3 모델에서는 이 조항이 더 이상 필요하지 않습니다.
Analysis
Root cause는 source-list 매칭의 빈-URL fast path에 있는 로직 오류입니다. 패치 이전, ContentSecurityPolicySourceListDirective::allows()는 빈 URL에 대해 shouldAllowEmptyURLIfSourceListEmpty == Yes && !m_sourceList.isNone()라는 predicate로 short-circuit했는데, 이는 지금은 제거된 주석이 언급한 §6.1.9 special case — URL 없는 플러그인은 'none'에 의해서만 차단되고 그 외에는 허용된다 — 를 구현한 것입니다.
이 predicate는 서로 다른 두 CSP 상태를 혼동합니다. m_sourceList.isNone()은 리터럴 'none' 토큰일 때만 true이므로, 빈 source list는 이를 false로 평가하고 predicate는 Yes && !false = true가 됩니다.
Policy isNone() empty-URL fast path Spec says
──────────────────── ──────── ───────────────────── ─────────
object-src 'none'; true Yes && !true = false block ✓
object-src; false Yes && !false = true block ✗ allowed
object-src 'self'; false Yes && !false = true allow ✓
가운데 행이 버그입니다. object-src; 정책이 있어도, data/src가 없는 <object>/<embed>가 기본 플러그인을 로드할 수 있었습니다. 작성자가 모든 object source를 금지하려고 정책을 작성했음에도 그랬습니다. isNone()이 리터럴 토큰일 때만 true라는 사실은 이 버그의 핵심 메커니즘이며, 제거된 predicate 자체의 semantics에서 도출된 것입니다. ContentSecurityPolicySourceList의 구현 자체는 제공된 context에는 포함되어 있지 않습니다.
패치는 이 special case를 매처에서 완전히 걷어내고, 처리를 한 단계 위로 옮깁니다. allows()가 빈 URL에 대해 항상 false를 반환하게 되면서, allowObjectFromSource()는 문서 자체의 protected URL을 대신 사용합니다. 이렇게 치환된 URL은 이후 일반적인 매처를 그대로 통과합니다. 'none'이나 빈 list와는 매치되지 않아 차단되고(둘 다 아무것도 매치하지 않으므로), 'self'나 wildcard와는 매치되어 허용됩니다. 결과적으로 별도의 전용 branch 없이도 §6.1.9가 의도했던 동작이 permissive 정책에 대해 재현됩니다.
Exploitation은 메모리 안전성 문제가 아니라 정책 우회에 해당합니다. Content-Security-Policy: object-src;로 보호된 페이지에서, HTML injection sink를 가진 공격자가 data/src attribute 없는 <object type="..."></object>나 <embed type="...">를 주입합니다. 패치 이전에는 빈-URL 경로가 true를 반환했고 allowObjectFromSource()가 인스턴스화를 허용했으므로, deny-all 정책에도 불구하고 type attribute가 선택한 플러그인이 실행될 수 있었습니다. 그 결과 WebContent process 내에서 후속 exploit이 이용할 수 있는 플러그인 attack surface가 넓어집니다. Sandbox escape는 아닙니다. CSP로 보호된 페이지에 기존 injection vector가 존재해야 하고, 그 페이지 정책 하에서 어떤 플러그인이 인스턴스화될 수 있는지만 완화되기 때문입니다.
발견 경위는 WPT conformance 작업에 가까워 보입니다. 새 테스트들은 imported/w3c/web-platform-tests/content-security-policy/object-src/ 아래에 추가되었고, CSP Level 3 §6.7.2.7이 명시적으로 요구하는 동작을 검증합니다. 이는 upstream WPT를 import하다가 WebKit의 실패를 발견했거나, 리뷰어가 §6.1.9 주석이 이미 대체된 editor's draft를 참조하고 있다는 점을 알아챘거나 둘 중 하나로 볼 수 있습니다. Level 3 §6.7.2.7 인용 자체는 commit message에서 그대로 가져온 내용입니다.
이 취약점은 CSP object-src defense-in-depth 경계를 약화시킵니다. 스펙은 object-src;와 object-src 'none'이 상호 교환 가능하다고 전제합니다. 둘 다 모든 플러그인 인스턴스화를 금지해야 하는데, 패치 이전에는 URL 없는 플러그인 경로에서 이 불변식이 깨져 있었습니다. 이는 object/embed 기반 exploitation을 무력화하기 위해 종종 배포되는 mitigation을 무너뜨리는 결과로 이어집니다.
Insight: 이 버그는 일종의 화석입니다. CSP Level 1/2에는 URL 없는 플러그인 element를 위한 리터럴 §6.1.9 special case가 있었는데, 이는 빈 source list를 균일하게 deny-all로 처리하는 더 간결한 Level 3 모델이 등장하기 이전의 것입니다. 이 legacy case를 매처 안으로 그대로 끌고 들어온 결과, 의미상 동등한 두 정책이 서로 다른 결정을 내리는 상태가 만들어졌습니다. 패치가 택한 전략 — 리소스 URL이 비어 있을 때 문서 자체의 URL로 치환하는 방식 — 은 참고할 가치가 있습니다. 별도의 전용 로직 없이도 동일한 매처가 URL 없는 케이스를 처리할 수 있게 되는데, 문서 URL이 'none'이나 빈 list와는 자연스럽게 매치되지 않으면서도 'self'나 wildcard와는 매치되기 때문입니다.
Audit directions
- 연관 URL이 없거나 opaque origin인 입력에 대한 legacy spec special case를 그대로 갖고 있는 security-policy 매처들. 다른 source-list consumer들(
violatedDirectiveForFrame,violatedDirectiveForMedia,violatedDirectiveForConnectSource등)과ContentSecurityPolicySourceList::matches경로에서, 명시적'none'토큰과 빈 source list 사이에 결과가 갈릴 수 있는url.isEmpty(),protocolIsAbout(),isOpaque()기반 branch가 있는지 점검해야 합니다.Source/WebCore/page/csp/ContentSecurityPolicySourceList.cpp부터 시작해isNone()호출자를 검색하는 방식을 권합니다. - "상태 X가 set되어 있음"과 "상태 X가 trivial하지 않음"을 혼동하는
flag == Yes && !state.isX()형태의 predicate들. CSP와 다른 policy 코드에서, negation이 적용되는 enum이나 flag의false값이 여러 의미적으로 다른 케이스 —'none'리터럴 vs. 빈 list, default vs. unset — 를 모두 포괄하는 유사한 two-condition gate가 있는지 검토할 필요가 있습니다. WebCore에서 security decision을 가드하는 boolean condition 안의!.*isNone(),!.*isEmpty()패턴을 검색해보는 것을 권합니다. 코드 리뷰 시, security gate 안에서 negate된isX()가 나온다면 그것을 false로 만드는 모든 상태를 명시하는 주석이 필요합니다. - "연관 리소스 없음"을 의미하기 위해 빈
URL을 합성하는 호출자들.ContentSecurityPolicy.h의allowObjectFromSource와 그 외allowXFromSource진입점의 consumer들을 점검하고, 각 호출자가 document-URL fallback 하에서도 합당한 결정을 내리는지(혹은 빈 URL을 아예 거부하는지) 확인해야 합니다.HTMLPlugInElement::allowedToLoadFrameURL과LoadedFromPluginElement::Yes로 표시된CachedResourceLoader::canRequest경로부터 시작하는 것을 권합니다. - 전달 방식(delivery-mode)과 directive 간 대칭성. meta로 전달되는 CSP와 header로 전달되는 CSP 사이의 parity를 새 WPT 테스트 케이스를 두 delivery mode 모두에서 재실행해 검증하고, 요청 URL이 없거나
about:blank일 때frame-src,media-src,connect-src가'none'과 빈 source list에 대해 동등한 차단 결정을 내리는지 확인해야 합니다.