CVE-2026-11166
Overview
Files Changed
third_party/blink/renderer/core/svg/svg_script_element.ccthird_party/blink/web_tests/external/wpt/dom/nodes/moveBefore/script-move-before.html
Patch
From 08939d0c1974c48749329076bbbc102c5979b37f Mon Sep 17 00:00:00 2001
From: Mike West <mkwst@chromium.org>
Date: Tue, 14 Apr 2026 05:36:00 -0700
Subject: [PATCH] Block synchronous script execution in SVGScriptElement during moveBefore.
SVGScriptElement::ChildrenChanged failed to check if a state-preserving
atomic move was in progress before triggering script evaluation. This
could lead to synchronous script execution during Node.moveBefore(),
violating security invariants and potentially confusing various other
parts of the system which rely upon them (`<iframe>` reparenting, etc).
This CL adds the missing StatePreservingAtomicMoveInProgress() guard,
mirroring the behavior of HTMLScriptElement.
Bug: 502118936
Change-Id: I6181c0afb641cda5b9da0696a3a5fdd005129974
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7761782
Reviewed-by: Fredrik Söderquist <fs@opera.com>
Commit-Queue: Mike West <mkwst@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1614386}
---
diff --git a/third_party/blink/renderer/core/svg/svg_script_element.cc b/third_party/blink/renderer/core/svg/svg_script_element.cc
index e19efaf..b5c21266 100644
--- a/third_party/blink/renderer/core/svg/svg_script_element.cc
+++ b/third_party/blink/renderer/core/svg/svg_script_element.cc
@@ -99,7 +99,9 @@
void SVGScriptElement::ChildrenChanged(const ChildrenChange& change) {
SVGElement::ChildrenChanged(change);
- loader_->ChildrenChanged(change);
+ if (!GetDocument().StatePreservingAtomicMoveInProgress()) {
+ loader_->ChildrenChanged(change);
+ }
// We'll record whether the script element children were ever changed by
// the API (as opposed to the parser).
diff --git a/third_party/blink/web_tests/external/wpt/dom/nodes/moveBefore/script-move-before.html b/third_party/blink/web_tests/external/wpt/dom/nodes/moveBefore/script-move-before.html
new file mode 100644
index 0000000..64bb3a51
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/dom/nodes/moveBefore/script-move-before.html
@@ -0,0 +1,35 @@
+<!DOCTYPE html>
+<title>ScriptElement moveBefore synchronous script execution</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<body>
+<script id="htmlScript"></script>
+<svg><script id="svgScript"></script></svg>
+<script>
+ test(() => {
+ const htmlScript = document.getElementById('htmlScript');
+ window.__ranHTMLInsideMoveBefore = false;
+ const textNode = document.createTextNode(`
+ window.__ranHTMLInsideMoveBefore = true;
+ `);
+ document.body.appendChild(textNode);
+
+ htmlScript.moveBefore(textNode, null);
+
+ assert_false(window.__ranHTMLInsideMoveBefore, "<html:script> does not define moving steps which allow script execution.");
+ }, "Synchronous script execution in HTMLScriptElement during moveBefore should be blocked");
+
+ test(() => {
+ const svgScript = document.getElementById('svgScript');
+ window.__ranSVGInsideMoveBefore = false;
+ const textNode = document.createTextNode(`
+ window.__ranSVGInsideMoveBefore = true;
+ `);
+ document.body.appendChild(textNode);
+
+ svgScript.moveBefore(textNode, null);
+
+ assert_false(window.__ranSVGInsideMoveBefore, "<svg:script> does not define moving steps which allow script execution.");
+ }, "Synchronous script execution in SVGScriptElement during moveBefore should be blocked");
+</script>
+</body>
Regression Test / PoC
diff --git a/third_party/blink/web_tests/external/wpt/dom/nodes/moveBefore/script-move-before.html b/third_party/blink/web_tests/external/wpt/dom/nodes/moveBefore/script-move-before.html
new file mode 100644
index 0000000..64bb3a51
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/dom/nodes/moveBefore/script-move-before.html
@@ -0,0 +1,35 @@
+<!DOCTYPE html>
+<title>ScriptElement moveBefore synchronous script execution</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<body>
+<script id="htmlScript"></script>
+<svg><script id="svgScript"></script></svg>
+<script>
+ test(() => {
+ const htmlScript = document.getElementById('htmlScript');
+ window.__ranHTMLInsideMoveBefore = false;
+ const textNode = document.createTextNode(`
+ window.__ranHTMLInsideMoveBefore = true;
+ `);
+ document.body.appendChild(textNode);
+
+ htmlScript.moveBefore(textNode, null);
+
+ assert_false(window.__ranHTMLInsideMoveBefore, "<html:script> does not define moving steps which allow script execution.");
+ }, "Synchronous script execution in HTMLScriptElement during moveBefore should be blocked");
+
+ test(() => {
+ const svgScript = document.getElementById('svgScript');
+ window.__ranSVGInsideMoveBefore = false;
+ const textNode = document.createTextNode(`
+ window.__ranSVGInsideMoveBefore = true;
+ `);
+ document.body.appendChild(textNode);
+
+ svgScript.moveBefore(textNode, null);
+
+ assert_false(window.__ranSVGInsideMoveBefore, "<svg:script> does not define moving steps which allow script execution.");
+ }, "Synchronous script execution in SVGScriptElement during moveBefore should be blocked");
+</script>
+</body>
Original Bug Report
UXSS via synchronous JS execution in SVGScriptElement during moveBefore
Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports without the Chrome Security team.
Overview: SVGScriptElement::ChildrenChanged lacks a check for StatePreservingAtomicMoveInProgress, allowing synchronous JavaScript execution during a moveBefore() operation. Executing script while this document-wide flag is true allows an attacker to bypass iframe detachment and structural security checks, leading to a desynchronization between the DOM tree and the Frame tree. This can be exploited to bypass Site Isolation and Permissions Policies.
Affected files:
third_party/blink/renderer/core/svg/svg_script_element.ccthird_party/blink/renderer/core/dom/node.ccthird_party/blink/renderer/core/dom/container_node.ccthird_party/blink/renderer/core/script/script_loader.ccthird_party/blink/renderer/core/html/html_frame_element_base.cc
Estimated timestamp from git blame: 2024-08-13
Description
A potential vulnerability exists in the handling of the new moveBefore DOM API when interacting with SVG <script> elements.
When a node is moved using moveBefore, Blink sets a document-wide flag, StatePreservingAtomicMoveInProgress, to true to ensure that state (such as iframe contents) is preserved and script execution is suspended during the transition. However, SVGScriptElement::ChildrenChanged fails to check this flag.
If an attacker uses moveBefore to move a text node containing a malicious payload into an already-connected, previously empty SVGScriptElement, SVGScriptElement::ChildrenChanged unconditionally triggers script evaluation via ScriptLoader::ChildrenChanged. Because the ScriptForbiddenScope used for the low-level DOM mutation has already been destroyed at this point in the insertion lifecycle, V8 executes the inline script synchronously.
Security Impact
Executing JavaScript while StatePreservingAtomicMoveInProgress is globally true is highly dangerous because several critical layout and security invariants are disabled based on this flag:
- Frame Detachment Bypass: If the executing script calls
removeChildon aniframe, theChildFrameDisconnectoris skipped. The iframe is removed from the DOM, but its underlyingFrameTreenode andContentFrameare not destroyed, andunloadevents do not fire. - Security Check Bypass: If the script subsequently calls
appendChildto insert the iframe into a new parent, it triggersHTMLFrameElementBase::InsertedInto. This method contains a critical check:SECURITY_CHECK(!ContentFrame() || GetDocument().StatePreservingAtomicMoveInProgress());. Because the flag is true, this check is bypassed.
By combining these primitives, an attacker can perform a “manual” state-preserving move, re-parenting an iframe in the DOM tree without updating its structural position in the renderer’s FrameTree or communicating the change to the browser process. This creates a severe desynchronization that can be leveraged to bypass Site Isolation, Same-Origin Policy checks, and Permissions Policies, leading to a Universal Cross-Site Scripting (UXSS) and renderer sandbox compromise.
(Note: These are potential steps based on code analysis; a working proof-of-concept has not been executed by our tooling.)
Suggested Steps to Reproduce
- Create an HTML document containing an
iframepointing to a cross-origin target. - Create an
svgelement containing an emptyscriptelement (<svg><script id="svgScript"></script></svg>) and append it to the document. - Create a
TextNodecontaining a JavaScript payload that removes theiframeand appends it to a different cross-origin parent. - Call
svgScript.moveBefore(text_node, null). - The payload executes synchronously, completing the malicious iframe re-parenting and establishing the UXSS primitive.
Suggested Fix
Update SVGScriptElement::ChildrenChanged (in third_party/blink/renderer/core/svg/svg_script_element.cc) to mirror the check performed in HTMLScriptElement::ChildrenChanged:
void SVGScriptElement::ChildrenChanged(const ChildrenChange& change) {
SVGElement::ChildrenChanged(change);
if (!GetDocument().StatePreservingAtomicMoveInProgress()) {
loader_->ChildrenChanged(change);
}
children_changed_by_api_ |= !change.ByParser();
}
Evaluated with Chrome root at commit: 096fc8fdbfacf2546485756d03f160a3d04fcc9b
Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.