CVE-2026-64787
Overview
Advisory: Apple Security Advisory
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected process termination
Researchers: 杉山 壮太, Shubham Chaskar
| Attribute | Value |
|---|---|
| CVE | CVE-2026-64787 |
| Bugzilla | 313703 |
| Component | WebCore |
| Bug Class | UAF |
| Severity | medium |
| Commit | cb83583e5f4d9553… |
| Advisory | Apple Advisory |
Root Cause Analysis
trustedTypeCompliantString executes arbitrary JavaScript via Trusted Types policy callbacks. The callers in Document::parseHTMLUnsafe, Document::write, and Document::execCommand passed a raw Document* from contextDocument() without protecting its lifetime. If the policy callback manipulates the DOM — adopting nodes, removing iframes, or triggering GC — the Document can be freed while trustedTypeCompliantString still holds and later dereferences the dangling pointer. The fix wraps contextDocument() with protect() to create a strong reference that keeps the Document alive for the entire duration of the trustedTypeCompliantString call.
Attack Path
1. Serve a page with a trusted-types iframe
Attacker serves a page containing an iframe whose srcdoc sets CSP require-trusted-types-for 'script' and moves a DOM element to the parent frame, exposing the parent’s trustedTypes object.
2. Register a malicious default policy
The attacker calls createPolicy('default', ...) on the exposed trustedTypes object. The policy’s createHTML callback adopts the target element, removes the iframe, nulls the reference, and forces garbage collection.
3. Trigger trusted type validation
After the callback, the attacker performs a DOM operation that triggers trustedTypeCompliantString (e.g., setting innerHTML, calling document.write, or execCommand('insertHTML')).
4. Document freed during callback
Inside trustedTypeCompliantString, the malicious policy callback runs and frees the context Document via iframe removal and GC. The raw Document* held by the caller is now dangling.
5. UAF on context document access
When trustedTypeCompliantString returns and the caller accesses contextDocument() again, it dereferences the freed Document, causing a use-after-free and unexpected process termination.
Changed Functions
| Function | File | Change | Note |
|---|---|---|---|
Document::parseHTMLUnsafe |
Source/WebCore/dom/Document.cpp |
modified | Wrapped contextDocument() with protect() before passing to trustedTypeCompliantString to prevent UAF during Trusted Types callback. |
Document::write |
Source/WebCore/dom/Document.cpp |
modified | Wrapped contextDocument() with protect() before passing to trustedTypeCompliantString to prevent UAF during Trusted Types callback. |
Document::execCommand |
Source/WebCore/dom/Document.cpp |
modified | Wrapped contextDocument() with protect() before passing to trustedTypeCompliantString to prevent UAF during Trusted Types callback. |
trusted-types-iframe-removal-crash.html |
LayoutTests/fast/dom/trusted-types-iframe-removal-crash.html |
added | Regression test: iframe removal during Trusted Types policy callback must not cause UAF. |
trusted-types-iframe-removal-crash-expected.txt |
LayoutTests/fast/dom/trusted-types-iframe-removal-crash-expected.txt |
added | Test expectation: PASS with no ASAN crash. |
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
Patch Preview
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();