← All issues

[3] HTMLPlugInElement skips content extensions for embed/object loads

Severity: Medium | Component: WebCore HTML plugin elements | 7d28e39

<embed>/<object> 마크업을 사용할 수 있는 모든 페이지에서 항상 재현 가능한 content blocker bypass를 수정하는 패치입니다. 영향 범위는 memory corruption이 아닌 policy 우회(사용자 설치 규칙을 벗어나는 tracker/malware/광고 로드)이므로 Medium으로 평가합니다.

<embed>, <object> 요소를 통해 로드되는 plugin content가 content extension 규칙 검사를 전혀 거치지 않았습니다. 이번 수정에서는 HTMLPlugInElement::canLoadURL에 content extension 검사가 추가되어, updateWidgetwouldLoadAsPlugIn 지연 처리 이전에 차단 대상 URL을 거부하도록 변경되었습니다.

Source/WebCore/html/HTMLPlugInElement.cpp

+#if ENABLE(CONTENT_EXTENSIONS)
+#include "ContentExtensionsBackend.h"
+#include "ResourceLoadInfo.h"
+#include "UserContentProvider.h"
+#endif
...
bool HTMLPlugInElement::canLoadURL(const URL& completeURL) const
{
+#if ENABLE(CONTENT_EXTENSIONS)
+ if (completeURL.isValid()) {
+ RefPtr page = document().page();
+ RefPtr frame = document().frame();
+ RefPtr documentLoader = frame ? frame->loader().documentLoader() : nullptr;
+ RefPtr userContentProvider = frame ? frame->userContentProvider() : nullptr;
+ if (page && documentLoader && userContentProvider) {
+ auto results = userContentProvider->processContentRuleListsForLoad(*page, completeURL, ContentExtensions::ResourceType::Other, *documentLoader);
+ if (results.shouldBlock())
+ return false;
+ }
+ }
+#endif
return !isProhibitedSelfReference(completeURL);
}

LayoutTests/http/tests/contentextensions/block-embed-element.html

+<embed src="http://127.0.0.1:8000/contentextensions/resources/should-load.html?should-be-blocked" type="text/html" width="100" height="100"></embed>
+<embed src="http://127.0.0.1:8000/media/resources/test.pdf?should-be-blocked" type="application/pdf" width="100" height="100"></embed>

ENABLE(CONTENT_EXTENSIONS)가 활성화된 상태에서 URL이 유효한 경우, canLoadURL은 document의 Page, DocumentLoader, UserContentProvider를 조회합니다. 이후 UserContentProvider::processContentRuleListsForLoad(page, completeURL, ContentExtensions::ResourceType::Other, documentLoader)를 호출하며, 반환된 결과의 shouldBlock()이 true이면 canLoadURL이 false를 반환하여 호출 측에서 plugin 리소스 로드를 건너뜁니다. 회귀 테스트는 .*should-be-blocked URL 필터에 매칭되는 <embed>, <object> 리소스를 로드하여, 표준 'Content blocker prevented frame...' 콘솔 메시지가 발생하는지 확인합니다.

리소스 로드 경로에서 content policy 검사가 누락되어, plugin element 클래스가 다른 element 유형에 균일하게 적용되는 필터를 우회할 수 있는 패턴.

Content Extensions(content blocker라고도 불림)는 WebKit 확장 기능 및 관리 구성에 선언형 규칙 목록 API를 제공합니다. 각 규칙은 trigger(URL 패턴, 리소스 유형, 로드 컨텍스트)와 action(block, block-cookies, css-display-none, make-https)의 쌍으로 이루어집니다. 로드 발생 시 UserContentProvider::processContentRuleListsForLoad가 모든 활성 규칙 목록을 평가하고 집계된 actions를 반환하는데, 호출 측은 results.shouldBlock()을 확인해 로드를 중단할지 결정합니다. HTMLPlugInElement<embed>, <object>, 레거시 <applet>의 C++ 베이스 클래스로, canLoadURL은 두 서브클래스 모두가 updateWidget 진입 전에 반드시 거치는 단일 진입점입니다. SubframeLoader에 plugin 또는 subframe widget 생성을 요청하기 이전에 이 함수를 통과하는 셈입니다. 한편 <iframe>, <img>, <script>, XHR/fetch 로드에 대한 content extension 훅은 각각의 loader 경로에 별도로 구현되어 있습니다.

HTMLPlugInElement::canLoadURL은 URL에 대해 self-reference 검사만 수행했을 뿐, content extension 엔진은 전혀 거치지 않았습니다. <img>, <script>, <iframe>, XHR 등 다른 리소스 유형에는 이미 규칙 목록 평가 경로가 존재했지만, plugin element 클래스에는 해당 경로가 연결되지 않은 상태였습니다.

공격자가 HTML을 직접 작성하거나 주입할 수 있는 상황을 가정합니다. 사용자가 설치한 content blocker가 차단했을 URL을 <embed src="BLOCKED_URL"> 또는 <object data="BLOCKED_URL"> 형태로 삽입하면, 패치 이전에는 그 로드가 그대로 진행되었습니다. HTMLPlugInElement::canLoadURLUserContentProvider를 전혀 거치지 않고 true를 반환했기 때문입니다. 그 결과 서드파티 tracker, malware 호스팅 도메인, 또는 사용자가 차단한 도메인의 PDF가 페이지 안에서 렌더링될 수 있었습니다. 추가된 processContentRuleListsForLoad(..., ResourceType::Other, ...) 호출은 SubframeLoader와 동일한 차단 지점에서 해당 로드를 거부합니다. 테스트 케이스는 .*should-be-blocked 필터에 대해 일반 <embed>, <object> 마크업을 실행하고, 새 경로에서 'Content blocker prevented frame...' 콘솔 메시지가 발생하는지 검증합니다.

이 vulnerability는 특정 URL이 페이지 내에서 로드되는 것을 막기 위해 사용자와 관리형 디바이스 정책이 의존하는 content blocker 신뢰 경계를 약화시켰습니다. HTML content extension 모델은 plugin element 로드를 포함한 모든 subresource 로드가 활성 규칙 목록에 대해 평가된다고 가정하는데, 패치 이전에는 <embed><object>에서 이 불변 조건이 성립하지 않았습니다. WebKit의 content extension 적용은 단일 로드 게이트에 집중되어 있지 않고, 네트워크 요청을 시작하는 각 element/loader 클래스에 개별적으로 복제됩니다. 이 구조가 적용 누락을 반복적으로 발생시키는 근본 원인입니다.