CVE-2026-20676
Overview
Background
- webkit-masked-url:// scheme
- A special URL scheme WebKit substitutes for resource URLs injected by Safari web extensions so page-visible APIs cannot observe the true (extension) URL.
- ResolveURLs serialization mode
- An enum controlling how attribute URLs are emitted during markup serialization — No leaves them literal, Yes resolves to absolute, and NoExcludingURLsForPrivacy leaves them literal but masks privacy-sensitive ones.
- XMLSerializer.serializeToString
- A DOM API exposed to page JavaScript that serializes a node subtree to an XML string, a code path that must obey the same privacy-masking rules as innerHTML.
- Browser fingerprinting / extension tracking
- Techniques by which a page detects installed extensions or injected content to build a persistent identifier for a user across sessions and sites.
Root Cause Analysis
The patch changes a single argument in XMLSerializer::serializeToString: the ResolveURLs mode passed to serializeFragment is changed from ResolveURLs::No to ResolveURLs::NoExcludingURLsForPrivacy. WebKit supports ‘masked’ URLs (the webkit-masked-url:// scheme) that Safari web extensions use to hide the true resource URL of content they inject into a page, so that the page’s own script cannot observe which extension is present or what real URL a resource has. The serialization layer therefore distinguishes several URL-handling modes: ResolveURLs::No leaves attribute URLs exactly as-is (which, for a masked attribute, means emitting the underlying real URL text rather than the masked placeholder), while ResolveURLs::NoExcludingURLsForPrivacy additionally suppresses/masks privacy-sensitive URLs during serialization.
The invariant that was violated is that DOM-serialization APIs reachable from page script must never reveal a privacy-masked URL’s true value. HTML serialization (innerHTML) already honored masking — the pre-existing test asserts document.body.innerHTML contains ‘<iframe src=“webkit-masked-url://hidden/">’ — but XMLSerializer.serializeToString used ResolveURLs::No and thus did not apply the privacy exclusion, so calling (new XMLSerializer()).serializeToString(document.body) leaked the real URL behind masked resources.
The fix restores the invariant by making the XML serializer use the privacy-aware mode, so the added test now expects the masked iframe to serialize as ‘webkit-masked-url://hidden/’ while a genuinely non-masked iframe still serializes as its real ‘http://apple.com/baz.html' URL. Because the vulnerable behavior of ResolveURLs::No vs NoExcludingURLsForPrivacy lives in the shared serializeFragment/markup-serialization code (not shown in this diff), the precise masking logic there is inferred, but the change of enum value and the new expectation string establish that XMLSerializer was previously emitting unmasked URLs.
Attack Path
- Victim has a Safari web extension that injects masked content The extension injects resources (e.g. an <iframe> or element) whose URLs are rewritten to webkit-masked-url:// so the page cannot see the true extension resource URL.
- Attacker page runs script in the same document A malicious or fingerprinting web page executes JavaScript in the page where the extension has injected masked content.
- Serialize the DOM via XMLSerializer The page calls (new XMLSerializer()).serializeToString(document.body) (or serializeToString on any subtree containing the masked node).
- Read the leaked real URL Because XMLSerializer used ResolveURLs::No, the returned XML string contains the true underlying URL instead of the webkit-masked-url:// placeholder, unlike innerHTML which correctly masks it.
- Fingerprint / track the user The attacker parses the serialized string to detect the presence and identity of specific extensions or extension-injected resources, using that as a stable signal to track or de-anonymize the user across visits.
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
XMLSerializer::serializeToStringSource/WebCore/xml/XMLSerializer.cpp |
modified | Changed the ResolveURLs argument to serializeFragment from ResolveURLs::No to ResolveURLs::NoExcludingURLsForPrivacy so XML serialization suppresses privacy-masked (webkit-masked-url://) URLs the way HTML/innerHTML serialization already did. |
TestWebViewConfiguration (WKWebViewConfiguration.mm test)Tools/TestWebKitAPI/Tests/WebKitCocoa/WKWebViewConfiguration.mm |
modified | Added an assertion that serializeToString(document.body) keeps the masked iframe as webkit-masked-url://hidden/ while a non-masked iframe keeps its real apple.com URL, pinning the corrected behavior. |
Files Changed
Source/WebCore/xml/XMLSerializer.cppTools/TestWebKitAPI/Tests/WebKitCocoa/WKWebViewConfiguration.mm
Audit Directions
- Other serializeFragment callers passing ResolveURLs::NoGrep WebCore for ‘ResolveURLs::No’ and for serializeFragment/createMarkup call sites (e.g. innerHTML, outerHTML, DOMParser round-trips, drag-and-drop and copy/paste markup) to find any that should use NoExcludingURLsForPrivacy but do not.
- All page-reachable paths that can reveal masked URLsSearch for ‘webkit-masked-url’, ‘maskedURL’, and ‘NoExcludingURLsForPrivacy’ and enumerate every API that can surface an element’s URL to script (getAttribute, attributes iteration, cssText, computed style, Range.toString, clipboard) to confirm each masks correctly.
- Privacy-masking consistency across serialization/URL-resolution enumsAudit the definition and use of the ResolveURLs enum and MarkupAccumulator to ensure the privacy mode is applied uniformly; grep ‘ResolveURLs’, ‘MarkupAccumulator’, and ‘appendURLAttribute’ for branches that bypass the privacy exclusion.