Medium chrome Memory Corruption 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactMemory corruption in DOM
DescriptionMemory corruption in DOM
ComponentDOM
Sub-componentBlink>DOM
Bug ClassMemory Corruption
Tracker553147798
Fix commit69ae35fad00a (chromium/src) +153/-0
CISA KEVNot listed
CreditedGoogle
Disclosed2026-10-20

Changed Functions

FunctionChangeNotes
CaretPositionTest
third_party/blink/renderer/core/css/cssom/caret_position_test.cc
modified
if
third_party/blink/renderer/core/css/cssom/caret_position_test.cc
modified
TEST_F
third_party/blink/renderer/core/css/cssom/caret_position_test.cc
modified
for
third_party/blink/web_tests/fast/forms/suggested-value-caret-position-from-point.html
modified

Files Changed

  • third_party/blink/renderer/core/html/resources/html.css
  • third_party/blink/renderer/core/css/cssom/caret_position_test.cc
  • third_party/blink/renderer/core/dom/document.cc
  • third_party/blink/web_tests/fast/forms/suggested-value-caret-position-from-point.html
From 69ae35fad00ad5f3c92ebd80c627a7c6af4c5c13 Mon Sep 17 00:00:00 2001
From: David Baron <[email protected]>
Date: Tue, 15 Sep 2026 09:35:40 -0700
Subject: [PATCH] Make autofill suggestion previews pointer-events:none.

This sets pointer-events:none on autofill suggestion previews so that
the suggested text cannot be computed using caretPositionFromPoint.

Everything except the document.cc changes are the AI-suggested fix; I
wrote the document.cc changes to replace an AI-suggested duplicate fix
with a CHECK().

I verified that both the unittest and the web test fail without the
html.css change.

Fixed: 553147798
Change-Id: Ia6e91f59a0cca3926aaa09442446d1b4c79d2339
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8392201
Commit-Queue: David Baron <[email protected]>
Reviewed-by: Joey Arhar <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1698172}
---
diff --git a/third_party/blink/renderer/core/dom/document.cc b/third_party/blink/renderer/core/dom/document.cc
index 2f00ea9..163385c 100644
--- a/third_party/blink/renderer/core/dom/document.cc
+++ b/third_party/blink/renderer/core/dom/document.cc
@@ -2090,6 +2090,10 @@
   Node* anchor_node = position_with_affinity.AnchorNode();
   if (TextControlElement* text_control = EnclosingTextControl(anchor_node)) {
     anchor_node = text_control;
+    CHECK(anchor_node == text_control ||
+          text_control->InnerEditorElement()->contains(anchor_node))
+        << "We should not expose positions in content (e.g., autofill "
+           "suggestions) not in the editor";
   }
   bool adjust_position = false;
   while (anchor_node->IsInShadowTree() &&
diff --git a/third_party/blink/renderer/core/html/resources/html.css b/third_party/blink/renderer/core/html/resources/html.css
index 6167ab3..c4adff7 100644
--- a/third_party/blink/renderer/core/html/resources/html.css
+++ b/third_party/blink/renderer/core/html/resources/html.css
@@ -634,6 +634,11 @@
     overflow: hidden;
 }
 
+input::-internal-input-suggested,
+textarea::-internal-input-suggested {
+    pointer-events: none !important;
+}
+
 @supports blink-feature(AutofillPreviewIgnoreAuthorFont) {
     input::-internal-input-suggested,
     textarea::-internal-input-suggested {
diff --git a/third_party/blink/renderer/core/css/cssom/caret_position_test.cc b/third_party/blink/renderer/core/css/cssom/caret_position_test.cc
index aff73dba..5c702a50 100644
--- a/third_party/blink/renderer/core/css/cssom/caret_position_test.cc
+++ b/third_party/blink/renderer/core/css/cssom/caret_position_test.cc
@@ -5,6 +5,7 @@
 #include "third_party/blink/renderer/core/css/cssom/caret_position.h"
 
 #include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/blink/renderer/bindings/core/v8/v8_caret_position_from_point_options.h"
 #include "third_party/blink/renderer/core/dom/range.h"
 #include "third_party/blink/renderer/core/geometry/dom_rect.h"
 #include "third_party/blink/renderer/core/html/forms/text_control_element.h"
@@ -16,6 +17,33 @@
 class CaretPositionTest : public PageTestBase {
  public:
   CaretPositionTest() = default;
+
+ protected:
+  // Sweeps caretPositionFromPoint() across the width of |control| and returns
+  // the distinct getClientRect() positions of the reported carets.
+  Vector<gfx::PointF> DistinctCaretRectPositions(TextControlElement& control) {
+    const DOMRect* bounds = control.GetBoundingClientRect();
+    const float y = bounds->top() + 12;
+    Vector<gfx::PointF> positions;
+    for (float x = bounds->left() + 4; x <= bounds->right() - 4; x += 2) {
+      CaretPosition* caret_position = GetDocument().caretPositionFromPoint(
+          x, y, CaretPositionFromPointOptions::Create());
+      if (!caret_position) {
+        continue;
+      }
+      EXPECT_EQ(&control, caret_position->offsetNode());
+      EXPECT_EQ(0u, caret_position->offset());
+      DOMRect* rect = caret_position->getClientRect();
+      if (!rect) {
+        continue;
+      }
+      gfx::PointF position(rect->x(), rect->y());
+      if (!positions.Contains(position)) {
+        positions.push_back(position);
+      }
+    }
+    return positions;
+  }
 };
 
 TEST_F(CaretPositionTest, offsetNodeAndOffset) {
@@ -117,4 +145,25 @@
   EXPECT_NE(nullptr, caret_position_client_rect);
   EXPECT_EQ(*range_client_rect, *caret_position_client_rect);
 }
+
+TEST_F(CaretPositionTest, caretPositionFromPointInInputWithSuggestedValue) {
+  SetBodyContent("<input style='width: 300px; font-size: 16px'>");
+  auto& input =
+      ToTextControl(*GetDocument().QuerySelector(AtomicString("input")));
+  input.SetSuggestedValue("suggested value 0123456789");
+  UpdateAllLifecyclePhasesForTest();
+
+  EXPECT_LE(DistinctCaretRectPositions(input).size(), 1u);
+}
+
+TEST_F(CaretPositionTest, caretPositionFromPointInTextareaWithSuggestedValue) {
+  SetBodyContent(
+      "<textarea style='width: 300px; font-size: 16px' rows='2'></textarea>");
+  auto& textarea =
+      ToTextControl(*GetDocument().QuerySelector(AtomicString("textarea")));
+  textarea.SetSuggestedValue("suggested value 0123456789");
+  UpdateAllLifecyclePhasesForTest();
+
+  EXPECT_LE(DistinctCaretRectPositions(textarea).size(), 1u);
+}
 }  // namespace blink
diff --git a/third_party/blink/web_tests/fast/forms/suggested-value-caret-position-from-point.html b/third_party/blink/web_tests/fast/forms/suggested-value-caret-position-from-point.html
new file mode 100644
index 0000000..f3fa8fc
--- /dev/null
+++ b/third_party/blink/web_tests/fast/forms/suggested-value-caret-position-from-point.html
@@ -0,0 +1,95 @@
+<!DOCTYPE html>
+<link rel="help" href="https://drafts.csswg.org/cssom-view/#dom-document-caretpositionfrompoint">
+<script src="../../resources/testharness.js"></script>
+<script src="../../resources/testharnessreport.js"></script>
+<style>
+  input, textarea {
+    width: 300px;
+    font-size: 16px;
+    font-family: monospace;
+  }
+</style>
+<input id="empty-input">
+<input id="filled-input" value="ab">
+<input id="password-input" type="password">
+<textarea id="text-area" rows="2"></textarea>
+<input id="placeholder-input" placeholder="placeholder text 0123456789">
+<input id="value-input" value="mm mm mm mm mm mm mm">
+<script>
+const suggestion = 'suggested value 0123456789';
+
+// Sweeps caretPositionFromPoint() across the width of |control| and returns
+// the set of distinct getClientRect() positions of the reported carets.
+function distinctCaretRectPositions(control) {
+  const bounds = control.getBoundingClientRect();
+  const y = bounds.top + 12;
+  const positions = new Set();
+  for (let x = bounds.left + 4; x <= bounds.right - 4; x += 2) {
+    const caretPosition = document.caretPositionFromPoint(x, y);
+    if (!caretPosition)
+      continue;
+    assert_equals(caretPosition.offsetNode, control,
+                  `offsetNode at (${x}, ${y})`);
+    assert_less_than_equal(caretPosition.offset, control.value.length,
+                           `offset at (${x}, ${y})`);
+    const rect = caretPosition.getClientRect();
+    if (rect)
+      positions.add(`${rect.x},${rect.y}`);
+  }
+  return positions;
+}
+
+function testSuggestedValue(control, maxDistinctPositions, description) {
+  test(() => {
+    control.focus();
+    internals.setSuggestedValue(control, suggestion);
+    try {
+      const positions = distinctCaretRectPositions(control);
+      assert_less_than_equal(
+          positions.size, maxDistinctPositions,
+          `distinct caret rects: ${JSON.stringify([...positions])}`);
+    } finally {
+      internals.setSuggestedValue(control, '');
+    }
+  }, description);
+}
+
+testSuggestedValue(
+    document.getElementById('empty-input'), 1,
+    'caretPositionFromPoint() over an empty <input> showing a suggested ' +
+        'value maps every point to the caret at the start of the value');
+
+testSuggestedValue(
+    document.getElementById('filled-input'),
+    document.getElementById('filled-input').value.length + 1,
+    'caretPositionFromPoint() over an <input> with a value showing a ' +
+        'suggested value only reports carets within the value');
+
+testSuggestedValue(
+    document.getElementById('password-input'), 1,
+    'caretPositionFromPoint() over an empty password <input> showing a ' +
+        'suggested value maps every point to the caret at the start of the ' +
+        'value');
+
+testSuggestedValue(
+    document.getElementById('text-area'), 1,
+    'caretPositionFromPoint() over an empty <textarea> showing a suggested ' +
+        'value maps every point to the caret at the start of the value');
+
+test(() => {
+  const control = document.getElementById('placeholder-input');
+  const positions = distinctCaretRectPositions(control);
+  assert_less_than_equal(
+      positions.size, 1,
+      `distinct caret rects: ${JSON.stringify([...positions])}`);
+}, 'caretPositionFromPoint() over an empty <input> showing a placeholder ' +
+       'maps every point to the caret at the start of the value');
+
+test(() => {
+  const control = document.getElementById('value-input');
+  const positions = distinctCaretRectPositions(control);
+  assert_greater_than(positions.size, 1,
+                      'caret rects should track positions within the value');
+}, 'caretPositionFromPoint() over an <input> with a value reports carets ' +
+       'at each position within the value');
+</script>
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/third_party/blink/renderer/core/css/cssom/caret_position_test.cc b/third_party/blink/renderer/core/css/cssom/caret_position_test.cc
index aff73dba..5c702a50 100644
--- a/third_party/blink/renderer/core/css/cssom/caret_position_test.cc
+++ b/third_party/blink/renderer/core/css/cssom/caret_position_test.cc
@@ -5,6 +5,7 @@
 #include "third_party/blink/renderer/core/css/cssom/caret_position.h"
 
 #include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/blink/renderer/bindings/core/v8/v8_caret_position_from_point_options.h"
 #include "third_party/blink/renderer/core/dom/range.h"
 #include "third_party/blink/renderer/core/geometry/dom_rect.h"
 #include "third_party/blink/renderer/core/html/forms/text_control_element.h"
@@ -16,6 +17,33 @@
 class CaretPositionTest : public PageTestBase {
  public:
   CaretPositionTest() = default;
+
+ protected:
+  // Sweeps caretPositionFromPoint() across the width of |control| and returns
+  // the distinct getClientRect() positions of the reported carets.
+  Vector<gfx::PointF> DistinctCaretRectPositions(TextControlElement& control) {
+    const DOMRect* bounds = control.GetBoundingClientRect();
+    const float y = bounds->top() + 12;
+    Vector<gfx::PointF> positions;
+    for (float x = bounds->left() + 4; x <= bounds->right() - 4; x += 2) {
+      CaretPosition* caret_position = GetDocument().caretPositionFromPoint(
+          x, y, CaretPositionFromPointOptions::Create());
+      if (!caret_position) {
+        continue;
+      }
+      EXPECT_EQ(&control, caret_position->offsetNode());
+      EXPECT_EQ(0u, caret_position->offset());
+      DOMRect* rect = caret_position->getClientRect();
+      if (!rect) {
+        continue;
+      }
+      gfx::PointF position(rect->x(), rect->y());
+      if (!positions.Contains(position)) {
+        positions.push_back(position);
+      }
+    }
+    return positions;
+  }
 };
 
 TEST_F(CaretPositionTest, offsetNodeAndOffset) {
@@ -117,4 +145,25 @@
   EXPECT_NE(nullptr, caret_position_client_rect);
   EXPECT_EQ(*range_client_rect, *caret_position_client_rect);
 }
+
+TEST_F(CaretPositionTest, caretPositionFromPointInInputWithSuggestedValue) {
+  SetBodyContent("<input style='width: 300px; font-size: 16px'>");
+  auto& input =
+      ToTextControl(*GetDocument().QuerySelector(AtomicString("input")));
+  input.SetSuggestedValue("suggested value 0123456789");
+  UpdateAllLifecyclePhasesForTest();
+
+  EXPECT_LE(DistinctCaretRectPositions(input).size(), 1u);
+}
+
+TEST_F(CaretPositionTest, caretPositionFromPointInTextareaWithSuggestedValue) {
+  SetBodyContent(
+      "<textarea style='width: 300px; font-size: 16px' rows='2'></textarea>");
+  auto& textarea =
+      ToTextControl(*GetDocument().QuerySelector(AtomicString("textarea")));
+  textarea.SetSuggestedValue("suggested value 0123456789");
+  UpdateAllLifecyclePhasesForTest();
+
+  EXPECT_LE(DistinctCaretRectPositions(textarea).size(), 1u);
+}
 }  // namespace blink
diff --git a/third_party/blink/web_tests/fast/forms/suggested-value-caret-position-from-point.html b/third_party/blink/web_tests/fast/forms/suggested-value-caret-position-from-point.html
new file mode 100644
index 0000000..f3fa8fc
--- /dev/null
+++ b/third_party/blink/web_tests/fast/forms/suggested-value-caret-position-from-point.html
@@ -0,0 +1,95 @@
+<!DOCTYPE html>
+<link rel="help" href="https://drafts.csswg.org/cssom-view/#dom-document-caretpositionfrompoint">
+<script src="../../resources/testharness.js"></script>
+<script src="../../resources/testharnessreport.js"></script>
+<style>
+  input, textarea {
+    width: 300px;
+    font-size: 16px;
+    font-family: monospace;
+  }
+</style>
+<input id="empty-input">
+<input id="filled-input" value="ab">
+<input id="password-input" type="password">
+<textarea id="text-area" rows="2"></textarea>
+<input id="placeholder-input" placeholder="placeholder text 0123456789">
+<input id="value-input" value="mm mm mm mm mm mm mm">
+<script>
+const suggestion = 'suggested value 0123456789';
+
+// Sweeps caretPositionFromPoint() across the width of |control| and returns
+// the set of distinct getClientRect() positions of the reported carets.
+function distinctCaretRectPositions(control) {
+  const bounds = control.getBoundingClientRect();
+  const y = bounds.top + 12;
+  const positions = new Set();
+  for (let x = bounds.left + 4; x <= bounds.right - 4; x += 2) {
+    const caretPosition = document.caretPositionFromPoint(x, y);
+    if (!caretPosition)
+      continue;
+    assert_equals(caretPosition.offsetNode, control,
+                  `offsetNode at (${x}, ${y})`);
+    assert_less_than_equal(caretPosition.offset, control.value.length,
+                           `offset at (${x}, ${y})`);
+    const rect = caretPosition.getClientRect();
+    if (rect)
+      positions.add(`${rect.x},${rect.y}`);
+  }
+  return positions;
+}
+
+function testSuggestedValue(control, maxDistinctPositions, description) {
+  test(() => {
+    control.focus();
+    internals.setSuggestedValue(control, suggestion);
+    try {
+      const positions = distinctCaretRectPositions(control);
+      assert_less_than_equal(
+          positions.size, maxDistinctPositions,
+          `distinct caret rects: ${JSON.stringify([...positions])}`);
+    } finally {
+      internals.setSuggestedValue(control, '');
+    }
+  }, description);
+}
+
+testSuggestedValue(
+    document.getElementById('empty-input'), 1,
+    'caretPositionFromPoint() over an empty <input> showing a suggested ' +
+        'value maps every point to the caret at the start of the value');
+
+testSuggestedValue(
+    document.getElementById('filled-input'),
+    document.getElementById('filled-input').value.length + 1,
+    'caretPositionFromPoint() over an <input> with a value showing a ' +
+        'suggested value only reports carets within the value');
+
+testSuggestedValue(
+    document.getElementById('password-input'), 1,
+    'caretPositionFromPoint() over an empty password <input> showing a ' +
+        'suggested value maps every point to the caret at the start of the ' +
+        'value');
+
+testSuggestedValue(
+    document.getElementById('text-area'), 1,
+    'caretPositionFromPoint() over an empty <textarea> showing a suggested ' +
+        'value maps every point to the caret at the start of the value');
+
+test(() => {
+  const control = document.getElementById('placeholder-input');
+  const positions = distinctCaretRectPositions(control);
+  assert_less_than_equal(
+      positions.size, 1,
+      `distinct caret rects: ${JSON.stringify([...positions])}`);
+}, 'caretPositionFromPoint() over an empty <input> showing a placeholder ' +
+       'maps every point to the caret at the start of the value');
+
+test(() => {
+  const control = document.getElementById('value-input');
+  const positions = distinctCaretRectPositions(control);
+  assert_greater_than(positions.size, 1,
+                      'caret rects should track positions within the value');
+}, 'caretPositionFromPoint() over an <input> with a value reports carets ' +
+       'at each position within the value');
+</script>
Loading diff…

Original Bug Report

reported by [email protected]

Autofill preview PII leak via document.caretPositionFromPoint().getClientRect()

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. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: Document::caretPositionFromPoint() fails to properly adjust positions inside text control UA shadow trees when the control is in the light DOM, retaining the raw UA-shadow position. Furthermore, the Autofill preview pseudo-element (::-internal-input-suggested) lacks a pointer-events: none rule, allowing the previewed text node to be hit-tested. Consequently, CaretPosition::getClientRect() constructs a collapsed Range on the suggested text node, potentially leaking character advance widths and coordinates of Autofill PII prior to user submission.

Affected files:

  • third_party/blink/renderer/core/dom/document.cc
  • third_party/blink/renderer/core/css/cssom/caret_position.cc
  • third_party/blink/renderer/core/html/resources/html.css

Estimated timestamp from git blame: 2024-05-15

Summary

A potential information disclosure vulnerability exists in Blink where document.caretPositionFromPoint().getClientRect() can be used to extract character advance widths and geometry of Autofill preview strings (PII) before the user submits or accepts the Autofill suggestion.

Root Cause Analysis

The issue arises from the interaction of two implementation details:

  1. Unadjusted Raw Shadow Position in Document::caretPositionFromPoint (third_party/blink/renderer/core/dom/document.cc:2090-2106):
Node* anchor_node = position_with_affinity.AnchorNode();
if (TextControlElement* text_control = EnclosingTextControl(anchor_node)) {
  anchor_node = text_control;
}
bool adjust_position = false;
while (anchor_node->IsInShadowTree() &&
       !(options->hasShadowRoots() &&
         options->shadowRoots().Contains(anchor_node->GetTreeScope()))) {
  anchor_node = anchor_node->OwnerShadowHost();
  adjust_position = true;
}
Position adjusted_position = adjust_position
                                 ? Position::InParentBeforeNode(*anchor_node)
                                 : position_with_affinity.GetPosition();
CHECK(!adjusted_position.IsNull());
return CreateCaretPosition(adjusted_position.ParentAnchoredEquivalent());

When hit-testing hits a node inside a text control’s UA shadow tree, EnclosingTextControl(anchor_node) returns the host <input> or <textarea>. However, if the control is in the light DOM, anchor_node->IsInShadowTree() evaluates to false. As a result, adjust_position remains false, and adjusted_position retains position_with_affinity.GetPosition() (the raw UA-shadow position). CreateCaretPosition (document.cc:1172-1175) then stores this raw node and offset in the CaretPosition object.

  1. Unsanitized Range Construction in CaretPosition::getClientRect() (third_party/blink/renderer/core/css/cssom/caret_position.cc:35-42):
DOMRect* CaretPosition::getClientRect() const {
  if (!node_) {
    return nullptr;
  }
  auto* range_object = MakeGarbageCollected<Range>(node_->GetDocument(), node_,
                                                   offset_, node_, offset_);
  return range_object->getBoundingClientRect();
}

While CaretPosition::offsetNode() and CaretPosition::offset() sanitize their outputs for text controls, getClientRect() constructs a collapsed Range directly on node_ and offset_. For collapsed ranges inside layout text, Range::getBoundingClientRect() returns a zero-width bounding box positioned at the character boundary (third_party/blink/renderer/core/layout/layout_text.cc:638-646, third_party/blink/renderer/core/dom/range.cc:1828-1830).

  1. Missing pointer-events: none on Suggested Values (third_party/blink/renderer/core/html/resources/html.css): While ::-webkit-input-placeholder defines pointer-events: none !important; (html.css:617), ::-internal-input-suggested (html.css:631-725) omits this rule. When an Autofill suggestion is previewed on hover, TextControlElement::SetSuggestedValue swaps the placeholder’s pseudo-element ID to ::-internal-input-suggested (third_party/blink/renderer/core/html/forms/text_control_element.cc:319-330), rendering the suggested PII text hit-testable.

Potential Attack Steps

(Note: These are suggested/potential steps derived from static code analysis; our tooling agent does not have code execution capabilities to verify dynamic runtime execution.)

  1. The attacker serves a webpage with an input field (e.g. <input type="text" name="name" autocomplete="name">) and runs a measurement script in requestAnimationFrame.
  2. The user focuses the input, triggering Chrome’s Autofill popup, and hovers over a profile suggestion.
  3. The browser process previews the suggestion in the renderer, populating the UA shadow DOM placeholder with the suggested PII text and pseudo-element ::-internal-input-suggested.
  4. Attacker JavaScript calls document.caretPositionFromPoint(x, y) across the input’s bounding box.
  5. Because the suggested text node is hit-tested and the raw UA-shadow position is preserved, caretPosition.getClientRect() returns the precise x coordinates of each character boundary in CSS viewport pixels.
  6. By measuring the difference between consecutive boundary positions ($X_{k+1} - X_k$), the attacker obtains the exact advance width of every character rendered in the UA-pinned font (font-family: sans-serif !important), enabling reconstruction of the PII string against dictionary candidates prior to user submission.

Suggested Fix

  1. In third_party/blink/renderer/core/html/resources/html.css, add pointer-events: none !important; to input::-internal-input-suggested and textarea::-internal-input-suggested.
  2. In third_party/blink/renderer/core/dom/document.cc (Document::caretPositionFromPoint), ensure positions inside text controls are properly mapped to light DOM equivalents (or host-relative positions) rather than preserving the raw UA-shadow position when adjust_position is evaluated.
  3. Alternatively, update CaretPosition::getClientRect() (third_party/blink/renderer/core/css/cssom/caret_position.cc:35-42) to use the remapped position if node_ belongs to a text control UA shadow tree.

Evaluated with Chrome root at commit: c6718ebbf2c677c6f63691e28c7801c563f6b19c


Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:

  • If you are familiar with the severity guidelines, you may adjust the severity.
  • If this is a false positive, and there’s no work to be done, please close as WAI.
  • If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.

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