Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInappropriate implementation in DOM
DescriptionInappropriate implementation in DOM
ComponentDOM
Bug ClassLogic Error
Tracker497964917
Fix commitbbc8fca2df94 (chromium/src) +22/-1
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-02

Files Changed

  • third_party/blink/renderer/core/html/forms/html_field_set_element.cc
  • third_party/blink/web_tests/external/wpt/dom/nodes/moveBefore/fieldset-child-blur-event.html
From bbc8fca2df94473ebaaef41d9b18964ad9d43304 Mon Sep 17 00:00:00 2001
From: Dominic Farolino <dom@chromium.org>
Date: Tue, 07 Apr 2026 12:38:27 -0700
Subject: [PATCH] DOM: Fix frame desynchronization bug

Script should not run during a state-preserving atomic move, i.e.,
during `Node::moveBefore()`. However, we did not catch the fact that
`HTMLFieldSetElement::ChildrenChanged()` calls `blur()` on the focused
element, and this CL prevents this from happening during an atomic
move. This is because neither focus should change, nor script should
run during this.

R=nrosenthal@google.com

Bug: 497964917
Change-Id: Ic7c1ec53df63a21e502b01130ee72395b6cb9fa0
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7736582
Reviewed-by: Noam Rosenthal <nrosenthal@google.com>
Commit-Queue: Dominic Farolino <dom@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1610937}
---

diff --git a/third_party/blink/renderer/core/html/forms/html_field_set_element.cc b/third_party/blink/renderer/core/html/forms/html_field_set_element.cc
index 13beb3c..6210bb1 100644
--- a/third_party/blink/renderer/core/html/forms/html_field_set_element.cc
+++ b/third_party/blink/renderer/core/html/forms/html_field_set_element.cc
@@ -189,8 +189,9 @@
         focused_element = element;
     }
   }
-  if (focused_element)
+  if (!GetDocument().StatePreservingAtomicMoveInProgress() && focused_element) {
     focused_element->blur();
+  }
 }
 
 FocusableState HTMLFieldSetElement::SupportsFocus(
diff --git a/third_party/blink/web_tests/external/wpt/dom/nodes/moveBefore/fieldset-child-blur-event.html b/third_party/blink/web_tests/external/wpt/dom/nodes/moveBefore/fieldset-child-blur-event.html
new file mode 100644
index 0000000..906d013
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/dom/nodes/moveBefore/fieldset-child-blur-event.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<title>moveBefore should not close a modal dialog</title>
+<link rel="author" title="Dominic Farolino" href="dom@chromium.org">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<body>
+<fieldset disabled id="fs">
+  <legend id="L1">
+    <input id="victim" onblur="window.blurExecuted = true">
+  </legend>
+  <legend id="L2"></legend>
+</fieldset>
+<script>
+test(() => {
+  victim.focus();
+  window.blurExecuted = false;
+  fs.moveBefore(L1, null);
+  assert_false(window.blurExecuted, "Blur event is not fired");
+}, "The 'blur' event is not fired on children of HTMLFieldSetElement during moveBefore()");
+</script>
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/third_party/blink/web_tests/external/wpt/dom/nodes/moveBefore/fieldset-child-blur-event.html b/third_party/blink/web_tests/external/wpt/dom/nodes/moveBefore/fieldset-child-blur-event.html
new file mode 100644
index 0000000..906d013
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/dom/nodes/moveBefore/fieldset-child-blur-event.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<title>moveBefore should not close a modal dialog</title>
+<link rel="author" title="Dominic Farolino" href="dom@chromium.org">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<body>
+<fieldset disabled id="fs">
+  <legend id="L1">
+    <input id="victim" onblur="window.blurExecuted = true">
+  </legend>
+  <legend id="L2"></legend>
+</fieldset>
+<script>
+test(() => {
+  victim.focus();
+  window.blurExecuted = false;
+  fs.moveBefore(L1, null);
+  assert_false(window.blurExecuted, "Blur event is not fired");
+}, "The 'blur' event is not fired on children of HTMLFieldSetElement during moveBefore()");
+</script>
Loading diff…

Original Bug Report

reported by vm...@google.com

Frame Tree Desynchronization via moveBefore() Synchronous Blur

Project Fortify, an experimental security project, has identified the following potential security issue.

Overview: A potential frame tree desynchronization vulnerability exists due to HTMLFieldSetElement::ChildrenChanged dispatching synchronous blur events while the StatePreservingAtomicMoveInProgress flag is active. This allows attacker-controlled JavaScript to execute during an atomic move, bypassing iframe lifecycle disconnections and security checks.

Affected files:

  • third_party/blink/renderer/core/html/forms/html_field_set_element.cc
  • third_party/blink/renderer/core/dom/node.cc
  • third_party/blink/renderer/core/dom/container_node.cc
  • third_party/blink/renderer/core/dom/document.cc
  • third_party/blink/renderer/core/html/html_frame_element_base.cc

Estimated timestamp from git blame: 2025-01-17

Core Vulnerability: A bypass of the StatePreservingAtomicMoveInProgress invariant permits attacker JavaScript execution during Node::moveBefore(), leading directly to frame tree desynchronization and potential Sandbox Escape / UXSS.

Technical Details

Initial logic and parameters surrounding Node::moveBefore(), the StatePreservingAtomicMoveInProgress flag assertion, and subsequent ContainerNode::AppendChild DOM tree manipulations are validated.

Standard processing applies during the mutation phases, culminating in the invocation of HTMLFieldSetElement::ChildrenChanged when a <legend> is moved within a disabled <fieldset>.

At this juncture, the logic determines the previously focused element inside the moved <legend> has become disabled. The EventDispatchForbiddenScope is explicitly closed. Control jumps directly to focused_element->blur(). Because Element::blur() lacks awareness of the atomic move state, it synchronously dispatches blur and focusout events to author JavaScript while GetDocument().StatePreservingAtomicMoveInProgress() remains true.

From within this event handler, an attacker can detach or re-insert arbitrary iframe elements. The active flag causes ContainerNode::WillRemoveChild to skip ChildFrameDisconnector, and HTMLFrameElementBase::InsertedInto to bypass SECURITY_CHECK(!ContentFrame() || GetDocument().StatePreservingAtomicMoveInProgress()). This results in a “zombie” iframe where the renderer detaches the element without notifying the browser’s RenderFrameHost.

Potential Attacker Steps (Unverified)

  1. Setup established context: <fieldset disabled id="fs"><legend id="L1"><input id="victim"></legend><legend id="L2"></legend></fieldset> alongside a target <iframe>.
  2. Standard processing applied: focus the victim input and attach a malicious blur event listener designed to call target.remove() or document.body.appendChild(target).
  3. Trigger the move: execute fs.moveBefore(L1, null).
  4. Vulnerability triggered: The synchronous blur event executes the payload while the atomic move flag is active, corrupting the iframe lifecycle state.

Suggested Fix

Instantiate a ScriptForbiddenScope directly within Node::moveBefore() to strictly enforce the invariant, or explicitly defer focus/blur updates in HTMLFieldSetElement::ChildrenChanged if StatePreservingAtomicMoveInProgress is active.

Evaluated with Chrome root at commit: a9cbf6e8b275fe4147435aa905f3b7f5a656f5f0


Results from 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