280ca3161bda00f116803a7064aba3ce91a7d59e Avoid dangling reference in HTMLAnchorElementBase::NavigateToHyperlink.
Triage note: Changed const AtomicString& target to a by-value copy of CleanNavigationTarget(GetEffectiveTarget()). WPT crash test shows a Trusted Types default policy calling setAttribute 64x, rehashing the attribute map and dangling the reference used in NavigateToHyperlink. Web-reachable via anchor click.
The bug at a glance
Use-after-free of ref-counted string storage in the Blink renderer, triggerable from a normal web page that installs a Trusted Types default policy. A const AtomicString& bound into an element’s attribute storage dangles when a reentrant setAttribute loop reallocates that storage during link navigation, giving a web-controlled UAF in the renderer. Renderer memory corruption is the standard stepping stone to sandbox-level compromise, rating this High.
Navigating an element to a javascript: URL under require-trusted-types-for ‘script’ forces the page’s Trusted Types default policy callback to run in the middle of HTMLAnchorElementBase::NavigateToHyperlink. That callback is attacker JavaScript, and it runs while the C++ code is still holding a reference that aliases the element’s attribute storage — a classic ‘user code runs under a borrowed reference’ reentrancy bug.
Root cause
In HTMLAnchorElementBase::NavigateToHyperlink the code bound ‘const AtomicString& target = frame_request.CleanNavigationTarget(GetEffectiveTarget());’, where GetEffectiveTarget() returns the element’s target attribute value living inside the element’s attribute storage, so target aliases the AtomicString held in that storage. Because the anchor points at a javascript: URL and the document enforces require-trusted-types-for ‘script’, the navigation invokes the Trusted Types default policy’s createScript callback — arbitrary page JavaScript — before target is consumed. That callback calls setAttribute many times on the same element, which reallocates the element’s attribute vector and destroys the Attribute (and its AtomicString) that target referenced, leaving target dangling into freed storage. The fix changes the binding to ‘const AtomicString target’ (a by-value copy), snapshotting the string’s ref-counted contents before the policy callback can reallocate the backing store.
Key code
Bind the navigation target by value instead of by reference (html_anchor_element.cc).
- const AtomicString& target =
+ const AtomicString target =
frame_request.CleanNavigationTarget(GetEffectiveTarget());
Patch walkthrough
third_party/blink/renderer/core/html/html_anchor_element.cc— Single-character-class fix: ‘const AtomicString&’ becomes ‘const AtomicString’. The reference bound to CleanNavigationTarget(GetEffectiveTarget())’s result — which aliases the element’s attribute storage — is replaced with a value copy, so the string survives any reallocation of the element’s attribute vector during the subsequent navigation (including the reentrant Trusted Types default-policy callback).third_party/blink/web_tests/external/wpt/trusted-types/navigate-to-javascript-url-009-crash.html— New WPT crash test. It installs a Trusted Types ‘default’ policy whose createScript handler fires 64 setAttribute calls on the target anchor and then drops its reference. The anchor has href=‘javascript:void(0)’, target=‘new-frame’, and 6 pre-existing data-original-* attributes; clicking it starts a navigation whose Trusted Types check runs createScript, whose setAttribute storm reallocates the attribute storage that NavigateToHyperlink’s target reference points into.
Background
AtomicString / attribute storage — An element’s attributes live in an AttributeCollection backed by a contiguous vector of Attribute objects, each holding ref-counted AtomicString name/value. Adding attributes can reallocate that vector, destroying the old Attribute objects and dropping their AtomicString refs; a reference aliasing one of those values becomes dangling.
Trusted Types default policy — Under require-trusted-types-for ‘script’, any sink that consumes a script string (including navigating to a javascript: URL) invokes the page-registered ‘default’ policy’s createScript callback — arbitrary author JavaScript run synchronously inside the C++ navigation path.
CleanNavigationTarget / GetEffectiveTarget — GetEffectiveTarget() returns the anchor’s effective target attribute value by reference into attribute storage; CleanNavigationTarget forwards it. The pre-fix caller held the result by const reference, aliasing live attribute storage.
javascript: URL navigation — Clicking an runs NavigateToHyperlink, and the javascript: scheme is what drags Trusted Types’ script check (and thus the default policy callback) into the middle of that function.
Vulnerability window
- Setup — Page sets CSP require-trusted-types-for ‘script’, registers a ‘default’ Trusted Types policy, and builds an with several data-* attributes.
- Trigger — target_element.click() starts the hyperlink navigation; NavigateToHyperlink binds target as a reference into the element’s attribute storage.
- Reentrancy — The javascript: URL forces the Trusted Types default createScript callback to run, which performs 64 setAttribute calls, reallocating the element’s attribute vector and freeing the AtomicString backing that target aliases.
- Use-after-free — Control returns to NavigateToHyperlink, which uses the now-dangling target reference for the navigation, reading freed string storage.
Proof of concept
This is the shipped WPT crash test and a complete web-facing trigger (no special flags beyond the CSP header). The 6 data-original-* attributes seed the attribute vector near a capacity boundary so the 64 setAttribute calls inside createScript force a reallocation, freeing the AtomicString that NavigateToHyperlink’s target reference points at. The test is labeled -crash; converting the reallocation-induced free into controlled reuse of the freed StringImpl slot is (inferred).
<!DOCTYPE HTML>
<meta http-equiv="Content-Security-Policy" content="require-trusted-types-for 'script'">
<body>
<script>
let target_element;
trustedTypes.createPolicy("default", {
createScript: input => {
if (target_element) {
for (let i = 0; i < 64; ++i)
target_element.setAttribute(`data-added-${i}`, "value");
target_element = undefined;
}
return input;
}
});
target_element = document.createElement("a");
target_element.href = "javascript:void(0)";
target_element.target = "new-frame";
for (let i = 0; i < 6; ++i)
target_element.setAttribute(`data-original-${i}`, "value");
document.body.appendChild(target_element);
target_element.click();
</script>
Exploitation
- Reach — Load an attacker page that enables require-trusted-types-for ‘script’ and registers a default policy, then create and click an so NavigateToHyperlink binds the dangling target reference.
- Free — Inside the createScript callback, run setAttribute in a loop to reallocate the anchor’s attribute vector, destroying the Attribute holding the target AtomicString and (if last ref) freeing its StringImpl.
- Use/groom — On return, the dangling target reference is read during navigation. Shaping the freed StringImpl slot with attacker-controlled data to turn the read into useful corruption is (inferred).
Detection & hunting
For defenders and detection engineers:
- ASAN heap-use-after-free in NavigateToHyperlink — A free stack through Element attribute setAttribute/reallocation inside a Trusted Types createScript callback, with the use stack in HTMLAnchorElementBase::NavigateToHyperlink reading the target AtomicString.
- Reentrant setAttribute during navigation — Flag pages that register a Trusted Types default policy and perform DOM mutations (setAttribute) on the navigating element from within createScript during a javascript: URL click.
Audit directions
- References aliasing attribute storage across reentrancy — Grep for ‘const AtomicString&’ / ‘const String&’ bound to attribute getters whose value is used after a call that can run author script (Trusted Types policies, mutation events, synchronous callbacks).
- Trusted Types default policy reentrancy — Audit every sink that funnels through a default-policy callback and verify the surrounding C++ holds copies, not references, of DOM-derived data across the callback.