Medium CVSS 6.5 webkit UAF 🔧 Commit mapped

Overview

Medium
Severity
6.5
CVSS
No
Exploited ITW
Fixed
Fix Status
DescriptionProcessing maliciously crafted web content may lead to an unexpected process termination
ComponentWebCore DOM
Bug ClassUAF
Tracker313703
Fix commitcb83583e5f4d (WebKit/WebKit) +50/-3
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:H
CISA KEVNot listed
Credited杉山 壮太, Shubham Chaskar
Disclosed2026-08-17

Background

Trusted Types
A CSP-driven mechanism (require-trusted-types-for ‘script’) that forces HTML/script sinks to go through a policy, invoking author JavaScript (createHTML) at the sink.
Default policy
A Trusted Types policy named ‘default’ whose createHTML callback runs synchronously whenever a guarded sink receives a plain string, giving attacker JS a foothold inside native code.
Re-entrancy
When native C++ calls back into JavaScript mid-operation; here the sink calls the default policy callback while still holding a bare Document pointer.
trustedTypeCompliantString
The WebCore helper that validates a string against Trusted Types; it may invoke the default policy (JS) and takes the context Document as a raw pointer.
contextDocument()
Returns the Document associated with the current context; a bare pointer whose lifetime is not guaranteed across a callback.
protect()
Wraps an object in a Ref/RefPtr so it is kept alive (ref-counted) across a scope — the fix wraps contextDocument() so it survives the Trusted Types callback.

Root Cause Analysis

This fixes a use-after-free of the context Document across a Trusted Types policy callback. Document::parseHTMLUnsafe(), Document::write()/writeln(), and Document::execCommand() call trustedTypeCompliantString(…, contextDocument(), …), which can invoke a web-author-provided Trusted Types default policy (createHTML) — arbitrary JavaScript. That callback can destroy the context document (the regression test removes the source iframe, adoptNode()s a node, nulls references, and forces GC inside createHTML).

Before the fix these call sites passed the raw contextDocument() pointer, so if the callback freed that document the subsequent use dangled, causing the unexpected process termination. The invariant is that a document passed into a routine that can run script must be kept alive for the duration of the call.

The fix wraps the argument in protect(contextDocument()) (a Ref/RefPtr protector) in all three call sites, keeping the context document alive across the Trusted Types callback. Fully established by the diff (the protect() helper’s definition is not shown but its protective intent is clear from usage).

Key insight
Trusted Types introduced a new synchronous JavaScript re-entrancy point (the default policy callback) at existing HTML sinks, but the callers still passed a bare, unprotected contextDocument() across it — the fix is to protect() the document at each sink so the long-standing ‘hold a raw Document across a call that can run script’ pattern is made safe.

Attack Path

  1. Set up a Trusted Types default policy The page (via an iframe with require-trusted-types-for ‘script’) installs a default policy whose createHTML callback runs attacker JavaScript.
  2. Free the context document inside the callback The createHTML callback removes the iframe, adopts nodes across documents, and forces GC so the context document is collected.
  3. Invoke a Trusted-Types-guarded API The page calls Document.write / execCommand(‘insertHTML’) / parseHTMLUnsafe, which passes the (now-freed) raw contextDocument() into trustedTypeCompliantString.
  4. Use-after-free After the callback frees the document, the caller dereferences the dangling contextDocument pointer, terminating the process.

Impact Assessment

The primitive is a use-after-free of the context Document, reached synchronously from Document.write/execCommand/parseHTMLUnsafe when the Trusted Types default policy callback frees the document. It is confined to the sandboxed WebContent process (advisory impact: unexpected process termination), so by itself it is a crash rather than a sandbox escape. Exploitability depends on whether the freed Document allocation can be reclaimed and type-confused before the dangling use (a reclaim-and-confuse UAF) versus only producing a controlled crash; the small, deterministic re-entrancy window and attacker control over the callback make targeted reclamation plausible, which is why it is treated as a memory-safety issue rather than a mere robustness bug.

Changed Functions

FunctionChangeNotes
Document::parseHTMLUnsafe
Source/WebCore/dom/Document.cpp
modified Wraps context.contextDocument() in protect(...) when calling trustedTypeCompliantString so the document survives the Trusted Types callback.
Document::write
Source/WebCore/dom/Document.cpp
modified Passes protect(contextDocument()) to trustedTypeCompliantString for write/writeln.
Document::execCommand
Source/WebCore/dom/Document.cpp
modified Passes protect(contextDocument()) to trustedTypeCompliantString on the insertHTML path.

Files Changed

  • LayoutTests/fast/dom/trusted-types-iframe-removal-crash-expected.txt
  • LayoutTests/fast/dom/trusted-types-iframe-removal-crash.html
  • Source/WebCore/dom/Document.cpp

Audit Directions

  • Other trustedTypeCompliantString callers
    Grep every call to trustedTypeCompliantString(…) in WebCore and confirm each passes a protected document/argument. The fix touched nine sinks; any caller passing contextDocument()/context.contextDocument() or a bare Document* without protect(…) is a candidate variant.
  • Bare Document across any script-invoking callback
    Look for native code that dereferences contextDocument()/document() after calling into anything that can run author JS (Trusted Types policies, custom elements reactions, MutationObserver, event dispatch, promise/microtask drains). The tell: a raw Document*/RefPtr captured before the callback and used after it without a Ref/Protector.
  • Objects reachable across detached-frame teardown
    Audit sinks reachable from a srcdoc/iframe whose document can be adopted away or GC’d inside the callback (as in the PoC: adoptNode + iframe.remove() + GCController.collect()). Any sibling state derived from the document (frame, loader, script controller) may also dangle.
  • Make the discipline structural
    Prefer converting call-site protect() discipline into API signatures that take Ref<Document>/protected references, so the compiler enforces liveness rather than relying on every caller remembering to protect().
diff --git a/LayoutTests/fast/dom/trusted-types-iframe-removal-crash-expected.txt b/LayoutTests/fast/dom/trusted-types-iframe-removal-crash-expected.txt
new file mode 100644
index 000000000000..fa64fd64ba1b
--- /dev/null
+++ b/LayoutTests/fast/dom/trusted-types-iframe-removal-crash-expected.txt
@@ -0,0 +1,3 @@
+This test passes if WebKit does not hit assertions or crash under ASAN
+
+PASS
diff --git a/LayoutTests/fast/dom/trusted-types-iframe-removal-crash.html b/LayoutTests/fast/dom/trusted-types-iframe-removal-crash.html
new file mode 100644
index 000000000000..05a5d282b2ca
--- /dev/null
+++ b/LayoutTests/fast/dom/trusted-types-iframe-removal-crash.html
@@ -0,0 +1,44 @@
+<!DOCTYPE html>
+<html>
+<body>
+<script>
+if (window.testRunner && window.GCController) {
+    testRunner.dumpAsText();
+    testRunner.waitUntilDone();
+
+    targetElement = document.createElement('div');
+
+    let iframe = document.createElement('iframe');
+
+    iframe.srcdoc = `<!DOCTYPE html>
+    <meta http-equiv="Content-Security-Policy" content="require-trusted-types-for \'script\'">
+    <body><script>document.body.appendChild(parent.targetElement); parent.innerTrustedTypes = trustedTypes;</` + 'script>';
+
+    iframe.onload = () => setTimeout(() => {
+        TrustedTypePolicyFactory.prototype.createPolicy.call(window.innerTrustedTypes, 'default', { createHTML: function () {
+            document.adoptNode(targetElement);
+            iframe.remove();
+            iframe = null;
+            GCController.collect();
+        } });
+        window.innerTrustedTypes = null;
+
+        GCController.collect();
+        try {
+            targetElement.innerHTML = 'x';
+        } catch (e) {
+            e.toString();
+        }
+
+        document.body.innerHTML = '<p>This test passes if WebKit does not hit assertions or crash under ASAN</p>PASS';
+
+        testRunner.notifyDone();
+    }, 0);
+
+    document.body.appendChild(iframe);
+} else
+    document.write('<p>This test requires testRunner and GCController</p>');
+
+</script>
+</body>
+</html>
\ No newline at end of file
diff --git a/Source/WebCore/dom/Document.cpp b/Source/WebCore/dom/Document.cpp
index 265e99637419..9ee254aa0d89 100644
--- a/Source/WebCore/dom/Document.cpp
+++ b/Source/WebCore/dom/Document.cpp
@@ -1229,7 +1229,7 @@ void Document::setMarkupUnsafe(const String& markup, OptionSet<ParserContentPoli
 
 ExceptionOr<Ref<Document>> Document::parseHTMLUnsafe(Document& context, Variant<Ref<TrustedHTML>, String>&& html)
 {
-    auto stringValueHolder = trustedTypeCompliantString(context.contextDocument(), WTF::move(html), "Document parseHTMLUnsafe"_s);
+    auto stringValueHolder = trustedTypeCompliantString(protect(context.contextDocument()), WTF::move(html), "Document parseHTMLUnsafe"_s);
     if (stringValueHolder.hasException())
         return stringValueHolder.releaseException();
 
@@ -4562,7 +4562,7 @@ ExceptionOr<void> Document::write(Document* entryDocument, FixedVector<Variant<R
     }
 
     String textString = text.toString();
-    auto stringValueHolder = trustedTypeCompliantString(TrustedType::TrustedHTML, contextDocument(), textString, lineFeed.isEmpty() ? "Document write"_s : "Document writeln"_s);
+    auto stringValueHolder = trustedTypeCompliantString(TrustedType::TrustedHTML, protect(contextDocument()), textString, lineFeed.isEmpty() ? "Document write"_s : "Document writeln"_s);
     if (stringValueHolder.hasException())
         return stringValueHolder.releaseException();
     SegmentedString trustedText(stringValueHolder.releaseReturnValue());
@@ -7853,7 +7853,7 @@ ExceptionOr<bool> Document::execCommand(const String& commandName, bool userInte
         [&commandName, this](const String& str) -> ExceptionOr<String> {
             if (commandName != "insertHTML"_s)
                 return String(str);
-            return trustedTypeCompliantString(TrustedType::TrustedHTML, contextDocument(), str, "Document execCommand"_s);
+            return trustedTypeCompliantString(TrustedType::TrustedHTML, protect(contextDocument()), str, "Document execCommand"_s);
         },
         [](const Ref<TrustedHTML>& trustedHtml) -> ExceptionOr<String> {
             return trustedHtml->toString();
Loading diff…

Original Bug Report

The reporter's bug is still restricted on the tracker.