← All issues

[2] CSP object-src empty-source-list bypass for no-URL plugin elements

Severity: Medium | Component: WebCore Content Security Policy enforcement | c6228ab

이 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

-bool ContentSecurityPolicySourceListDirective::allows(const URL& url, bool didReceiveRedirectResponse, ShouldAllowEmptyURLIfSourceListIsNotNone shouldAllowEmptyURLIfSourceListEmpty)
+bool ContentSecurityPolicySourceListDirective::allows(const URL& url, bool didReceiveRedirectResponse)
{
if (url.isEmpty())
- return shouldAllowEmptyURLIfSourceListEmpty == ShouldAllowEmptyURLIfSourceListIsNotNone::Yes && !m_sourceList.isNone();
+ return false;
return m_sourceList.matches(url, didReceiveRedirectResponse);
}

Source/WebCore/page/csp/ContentSecurityPolicy.cpp

- if (m_policies.isEmpty() || LegacySchemeRegistry::schemeShouldBypassContentSecurityPolicy(url.protocol()))
+ const auto& urlToCheck = url.isEmpty() ? m_protectedURL : url;
+ if (m_policies.isEmpty() || LegacySchemeRegistry::schemeShouldBypassContentSecurityPolicy(urlToCheck.protocol()))
return true;
- // ... 'MUST be blocked if object-src's value is 'none', but will otherwise be allowed' ...
String sourceURL;
- const auto& blockedURL = !preRedirectURL.isNull() ? preRedirectURL : url;
+ const auto& blockedURL = !preRedirectURL.isNull() ? preRedirectURL : urlToCheck;
...
- return allPoliciesAllow(handleViolatedDirective, &ContentSecurityPolicyDirectiveList::violatedDirectiveForObjectSource, url, redirectResponseReceived == RedirectResponseReceived::Yes, ContentSecurityPolicySourceListDirective::ShouldAllowEmptyURLIfSourceListIsNotNone::Yes);
+ return allPoliciesAllow(handleViolatedDirective, &ContentSecurityPolicyDirectiveList::violatedDirectiveForObjectSource, urlToCheck, redirectResponseReceived == RedirectResponseReceived::Yes);

LayoutTests/imported/w3c/web-platform-tests/content-security-policy/object-src/object-src-no-url-empty-source-list-blocked.html

+<meta http-equiv="Content-Security-Policy" content="object-src; script-src 'self' 'unsafe-inline';">
+<object type="text/html"></object>

패치는 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이 아닌 허용으로 동작한 패턴.

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을 인스턴스화할 수 있는데, datasrc 속성이 모두 없는 경우에는 type 속성만으로 plugin이 선택됩니다. 초기 CSP 초안에는 이러한 no-URL plugin에 대해 'none'이 명시되지 않은 한 허용하는 특수 조항이 있었으며, 이것이 이번 버그의 배경이 되었습니다.

패치 이전 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을 약화시키는 취약점이었습니다.

Note: 일부 내용(m_sourceList.isNone()이 리터럴 'none' 토큰에 대해서만 true를 반환한다는 점, allowObjectFromSource의 특정 호출 지점, §6.7.2.7 명세 인용)은 diff에 직접 드러나지 않으며, patch 주석과 주변 코드로부터 추론된 것입니다. 다만 bypass 메커니즘, 즉 빈 source list가 허용 분기에 도달하는 경로는 패치가 추가한 테스트 케이스를 통해 직접 확인됩니다.