Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInappropriate implementation in SVG
DescriptionInappropriate implementation in SVG
ComponentSVG
Bug ClassLogic Error
Tracker502118936
Fix commit08939d0c1974 (chromium/src) +38/-1
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-02

Files Changed

  • third_party/blink/renderer/core/svg/svg_script_element.cc
  • third_party/blink/web_tests/external/wpt/dom/nodes/moveBefore/script-move-before.html
From 08939d0c1974c48749329076bbbc102c5979b37f Mon Sep 17 00:00:00 2001
From: Mike West <[email protected]>
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 <[email protected]>
Commit-Queue: Mike West <[email protected]>
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>
Loading diff…

Regression Test / PoC

shipped with the fix
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>
Loading diff…

Original Bug Report

reported by [email protected]

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.cc
  • third_party/blink/renderer/core/dom/node.cc
  • third_party/blink/renderer/core/dom/container_node.cc
  • third_party/blink/renderer/core/script/script_loader.cc
  • third_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:

  1. Frame Detachment Bypass: If the executing script calls removeChild on an iframe, the ChildFrameDisconnector is skipped. The iframe is removed from the DOM, but its underlying FrameTree node and ContentFrame are not destroyed, and unload events do not fire.
  2. Security Check Bypass: If the script subsequently calls appendChild to insert the iframe into a new parent, it triggers HTMLFrameElementBase::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

  1. Create an HTML document containing an iframe pointing to a cross-origin target.
  2. Create an svg element containing an empty script element (<svg><script id="svgScript"></script></svg>) and append it to the document.
  3. Create a TextNode containing a JavaScript payload that removes the iframe and appends it to a different cross-origin parent.
  4. Call svgScript.moveBefore(text_node, null).
  5. 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.

View on issue tracker