← All issues

[10] DocumentLoader ScriptDisallowedScope Bypass via PluginView Destruction

Severity: Medium | Component: WebCore document loader | 7fec184

ScriptDisallowedScope 내에서 JavaScript가 실행된다는 점에서 Medium으로 평가합니다. Debug 빌드에서는 assertion failure가 발생하고, release 빌드에서는 조용히 re-entrancy가 발생합니다. 잠재적 escalation 경로로는 style recalculation 중 DOM mutation을 통한 memory corruption이 있습니다. 다만 이를 실현하려면 공격자의 JS callback이 recalculation call stack의 특정 stale reference를 건드려야 하며, 이론적으로는 가능하지만 검증되지 않은 시나리오입니다.

DocumentLoader::removePlugInStreamLoader()에서 checkLoadComplete()를 동기적으로 호출하던 방식이 document의 event loop에 비동기 task를 추가하는 방식으로 변경되었습니다. 새 코드는 m_framem_frame->document()가 null이 아닌지 먼저 확인하며, task 실행 전에 DocumentLoader가 소멸되지 않도록 Ref { *this }를 capture합니다.

Source/WebCore/loader/DocumentLoader.cpp

void DocumentLoader::removePlugInStreamLoader(ResourceLoader& loader)
{
ASSERT(m_plugInStreamLoaders.contains(&loader));
-
m_plugInStreamLoaders.remove(&loader);
- checkLoadComplete();
+ if (m_frame && m_frame->document()) {
+ protect(m_frame->document())->eventLoop().queueTask(TaskSource::Networking, [protectedThis = Ref { *this }]() {
+ protectedThis->checkLoadComplete();
+ });
+ }
}

LayoutTests/fast/pdf-plugin-destruction-dispatches-print-event-crash.html

+<embed id="embed" style="top: 200vmin; visibility: hidden;" type="application/pdf" src="about:blank"></embed>
+<script>
+ window.testRunner?.dumpAsText();
+ window.testRunner?.waitUntilDone();
+ window.addEventListener("load", _ => {
+ embed.setAttribute("onsubmit", "");
+ window.print();
+ requestAnimationFrame(_ => requestAnimationFrame(_ => window.testRunner?.notifyDone()));
+ });
+</script>

Style recalculation 중 ScriptDisallowedScope 내부에서 발생하는 동기적 script 유발 callback.

ScriptDisallowedScope는 WebKit에서 사용하는 RAII guard로, 해당 scope가 살아있는 동안 main thread에서 JavaScript가 실행되지 않음을 보장합니다. Debug 빌드에서는 assertion으로 강제되지만, release 빌드에서는 실행 자체를 막지 못합니다. 따라서 이 규칙을 위반하는 코드는 단순한 debug crash가 아니라 실제 attack surface가 됩니다.

Document::updateStyleIfNeeded()는 document의 style recalculation을 수행하는 함수입니다. 이 과정에서는 style/layout tree가 중간 상태에 있을 수 있기 때문에 ScriptDisallowedScope가 활성화됩니다. PluginView는 PDF viewer와 같은 embedded plugin을 나타냅니다. style 업데이트 결과 plugin element가 더 이상 렌더링되지 않는다고 판단되면 PluginView가 소멸되고, 이때 연결된 stream loader들이 DocumentLoader::removePlugInStreamLoader()를 통해 정리됩니다. checkLoadComplete()는 모든 subresource가 로딩을 완료했는지 확인하는 DocumentLoader 메서드로, event dispatch를 포함한 completion callback을 유발할 수 있습니다.

패치 이전에는 removePlugInStreamLoader()checkLoadComplete()를 동기적으로 호출했습니다. 이 함수는 ScriptDisallowedScope가 활성화된 updateStyleIfNeeded() 도중에 도달할 수 있었습니다. style 업데이트가 PluginView의 소멸을 유발하면 updateStyleIfNeeded() → PluginView 소멸 → removePlugInStreamLoader()checkLoadComplete()dispatchPrintEvent 순서의 호출 체인이 형성됩니다. 결과적으로 ScriptDisallowedScope 내부에서 JavaScript event listener가 실행되는 상황이 발생했습니다. release 빌드에서는 ScriptDisallowedScope가 이 invariant를 강제하지 않으므로, style recalculation 중 DOM/style tree가 중간 상태에 있는 동안 JavaScript가 조용히 실행됩니다.

🔒

Explores the re-entrancy implications and what an attacker could achieve with script execution during style recalculation

더 확인하려면 구독해 주세요

🔒

Multiple audit patterns identified for finding similar ScriptDisallowedScope violations across WebKit's object lifecycle and event dispatch paths

더 확인하려면 구독해 주세요