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.txt
  • LayoutTests/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();