High chrome Logic Error 🔧 Commit mapped

Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInformation leak in Canvas
DescriptionInformation leak in Canvas
ComponentCanvas
Bug ClassLogic Error
Tracker532904047
Fix commit584ed97cc815 (chromium/src) +1221/-19
CISA KEVNot listed
CreditedGoogle
Disclosed2026-08-25

Changed Functions

FunctionChangeNotes
promise_test
third_party/blink/web_tests/external/wpt/html/canvas/element/manual/draw-element-image/privacy/pseudos/before-after-images-ignored.tentative.https.sub.html
modified

Files Changed

  • third_party/blink/renderer/core/dom/element.cc
  • third_party/blink/renderer/core/dom/element.h
  • third_party/blink/renderer/core/paint/object_paint_invalidator.cc
  • third_party/blink/web_tests/external/wpt/html/canvas/element/manual/draw-element-image/privacy/pseudos/before-after-images-ignored.tentative.https.sub.html
From 584ed97cc8158833992d052aa92b0cd0b9eff8f5 Mon Sep 17 00:00:00 2001
From: Stephen Chenney <schenney@chromium.org>
Date: Tue, 18 Aug 2026 16:21:41 -0700
Subject: [PATCH] [HiC] Privacy protection for pseudo elements

Protect private content in pseudo elements by setting IsInCanvasSubtree
for all pseudo elements. Add tests to cover the following:
::first-line
::first-letter
::before and ::after
::marker
::details-content
::details-content::after (to test nested pseudos)

There are still more to test but land this first and then add more,
to get the protection in sooner. Testing everything will require
several more days.

Fixed: 532904047
Change-Id: I859b78904b7ac8980c548db593274c009c16fdb2
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8253249
Reviewed-by: Stefan Zager <szager@chromium.org>
Commit-Queue: Stephen Chenney <schenney@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1681817}
---

diff --git a/third_party/blink/renderer/core/dom/element.cc b/third_party/blink/renderer/core/dom/element.cc
index bf3a13f..5f721f1 100644
--- a/third_party/blink/renderer/core/dom/element.cc
+++ b/third_party/blink/renderer/core/dom/element.cc
@@ -4354,39 +4354,45 @@
 }
 
 #if DCHECK_IS_ON()
-void VerifySubtreeIsInCanvas(const Element& element, bool value) {
-  DCHECK(element.IsInCanvasSubtree() == value);
-  if (IsA<HTMLCanvasElement>(element)) {
-    DCHECK(element.IsCanvasOrInCanvasSubtree());
+void Element::VerifySubtreeIsInCanvas(bool value) {
+  DCHECK(IsInCanvasSubtree() == value);
+  if (IsA<HTMLCanvasElement>(this)) {
+    DCHECK(IsCanvasOrInCanvasSubtree());
     // When the verifier starts with an element outside the tree that should
     // have value false, but then reaches a canvas within the subtree (e.g.
     // in an iframe or nested), we should set the expected value back to true.
     value = true;
   }
-  if (ShadowRoot* shadow_root = element.GetShadowRoot()) {
+  if (ShadowRoot* shadow_root = GetShadowRoot()) {
     for (Element& child : ElementTraversal::ChildrenOf(*shadow_root)) {
-      VerifySubtreeIsInCanvas(child, value);
+      child.VerifySubtreeIsInCanvas(value);
     }
   }
-  if (auto* slot = ToHTMLSlotElementIfSupportsAssignmentOrNull(element)) {
+  if (auto* slot = ToHTMLSlotElementIfSupportsAssignmentOrNull(*this)) {
     for (Node* node : slot->AssignedNodesNoRecalc()) {
       if (auto* child = DynamicTo<Element>(node)) {
-        VerifySubtreeIsInCanvas(*child, value);
+        child->VerifySubtreeIsInCanvas(value);
       }
     }
   }
-  if (const auto* frame_owner = DynamicTo<HTMLFrameOwnerElement>(element)) {
+  if (const auto* frame_owner = DynamicTo<HTMLFrameOwnerElement>(this)) {
     if (Document* inner_document = frame_owner->contentDocument()) {
       if (Element* root = inner_document->documentElement()) {
-        VerifySubtreeIsInCanvas(*root, value);
+        root->VerifySubtreeIsInCanvas(value);
       }
     }
   }
-  for (Element& child : ElementTraversal::ChildrenOf(element)) {
+  if (const NodeRareData* rare_data = RareData()) {
+    for (PseudoElement* pseudo_element : rare_data->GetPseudoElements()) {
+      pseudo_element->VerifySubtreeIsInCanvas(value);
+    }
+  }
+
+  for (Element& child : ElementTraversal::ChildrenOf(*this)) {
     if (child.AssignedSlotWithoutRecalc()) {
       continue;
     }
-    VerifySubtreeIsInCanvas(child, value);
+    child.VerifySubtreeIsInCanvas(value);
   }
 }
 #endif
@@ -4395,7 +4401,7 @@
   if (value == IsInCanvasSubtree()) {
 #if DCHECK_IS_ON()
     if (!GetDocument().IsSlotAssignmentRecalcForbidden()) {
-      VerifySubtreeIsInCanvas(*this, value);
+      VerifySubtreeIsInCanvas(value);
     }
 #endif
     return;
@@ -4427,6 +4433,11 @@
     }
     child.SetIsInCanvasSubtree(value);
   }
+  if (const NodeRareData* rare_data = RareData()) {
+    for (PseudoElement* pseudo_element : rare_data->GetPseudoElements()) {
+      pseudo_element->SetIsInCanvasSubtree(value);
+    }
+  }
 }
 
 bool Element::ComputeIsInCanvasSubtree() const {
@@ -4469,12 +4480,9 @@
 void Element::DidChangeIsInCanvasSubtree() {
   if (auto* layout_object = GetLayoutObject()) {
     layout_object->SetNeedsPaintPropertyUpdate();
-    if (layout_object->HasLayer()) {
-      To<LayoutBoxModelObject>(layout_object)->Layer()->SetNeedsRepaint();
-    }
     ObjectPaintInvalidator(*layout_object)
-        .InvalidateDisplayItemClient(*layout_object,
-                                     PaintInvalidationReason::kUncacheable);
+        .SlowSetPaintingLayerNeedsRepaintAndInvalidateDisplayItemClient(
+            *layout_object, PaintInvalidationReason::kUncacheable);
   }
 }
 
diff --git a/third_party/blink/renderer/core/dom/element.h b/third_party/blink/renderer/core/dom/element.h
index f7353a1..ec6c85e3 100644
--- a/third_party/blink/renderer/core/dom/element.h
+++ b/third_party/blink/renderer/core/dom/element.h
@@ -1174,6 +1174,10 @@
   bool IsCanvasOrInCanvasSubtree() const;
   // Called when `IsInCanvasSubtree()` changes.
   virtual void DidChangeIsInCanvasSubtree();
+#if DCHECK_IS_ON()
+  void VerifySubtreeIsInCanvas(bool value);
+#endif
+
   HTMLCanvasElement* CanvasForDrawing() const;
 
   DOMMatrix* getCanvasTransform();
diff --git a/third_party/blink/renderer/core/paint/object_paint_invalidator.cc b/third_party/blink/renderer/core/paint/object_paint_invalidator.cc
index 57cbac0..f3d25357 100644
--- a/third_party/blink/renderer/core/paint/object_paint_invalidator.cc
+++ b/third_party/blink/renderer/core/paint/object_paint_invalidator.cc
@@ -34,7 +34,7 @@
   // It's caller's responsibility to ensure PaintingLayer's NeedsRepaint is
   // set. Don't set the flag here because getting PaintLayer has cost and the
   // caller can use various ways (e.g.
-  // PaintInvalidatinContext::painting_layer) to reduce the cost.
+  // PaintInvalidationContext::painting_layer) to reduce the cost.
   CheckPaintLayerNeedsRepaint();
 #endif
   TRACE_EVENT_INSTANT2(TRACE_DISABLED_BY_DEFAULT("blink.invalidation"),
diff --git a/third_party/blink/web_tests/external/wpt/html/canvas/element/manual/draw-element-image/privacy/pseudos/before-after-images-ignored.tentative.https.sub.html b/third_party/blink/web_tests/external/wpt/html/canvas/element/manual/draw-element-image/privacy/pseudos/before-after-images-ignored.tentative.https.sub.html
new file mode 100644
index 0000000..89b2191
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/html/canvas/element/manual/draw-element-image/privacy/pseudos/before-after-images-ignored.tentative.https.sub.html
@@ -0,0 +1,56 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>drawElementImage does not draw cross-origin images for before/after</title>
+  <script src='/resources/testharness.js'></script>
+  <script src='/resources/testharnessreport.js'></script>
+  <script src='/html/canvas/resources/canvas-tests.js'></script>
+  <script src="/html/canvas/resources/wait-for-canvas-paint.js"></script>
+  <style>
+    .wrapper {
+      width: 400px;
+      height: 120px;
+    }
+    .base::before {
+      content: url("https://{{hosts[alt][www]}}:{{ports[https][0]}}/images/red-100x100.png");
+      width: 100px;
+      height: 100px;
+      background-color: green;
+      display: inline-block;
+    }
+    .base::after {
+      content: url("https://{{hosts[alt][www]}}:{{ports[https][0]}}/images/red-100x100.png");
+      width: 100px;
+      height: 100px;
+      background-color: green;
+      display: inline-block;
+    }
+  </style>
+</head>
+<body>
+  <canvas id=canvas width="100" height="200" layoutsubtree>
+    <div id=child class=wrapper>
+      <p class=base>Before/After</p>
+    </div>
+  </canvas>
+
+  <script>
+    window.onload = () => {
+      promise_test(async function(t) {
+        await waitForCanvasPaint(canvas);
+        const context = canvas.getContext("2d");
+        context.drawElementImage(child, 0, 0);
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/third_party/blink/web_tests/external/wpt/html/canvas/element/manual/draw-element-image/privacy/pseudos/before-after-images-ignored.tentative.https.sub.html b/third_party/blink/web_tests/external/wpt/html/canvas/element/manual/draw-element-image/privacy/pseudos/before-after-images-ignored.tentative.https.sub.html
new file mode 100644
index 0000000..89b2191
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/html/canvas/element/manual/draw-element-image/privacy/pseudos/before-after-images-ignored.tentative.https.sub.html
@@ -0,0 +1,56 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>drawElementImage does not draw cross-origin images for before/after</title>
+  <script src='/resources/testharness.js'></script>
+  <script src='/resources/testharnessreport.js'></script>
+  <script src='/html/canvas/resources/canvas-tests.js'></script>
+  <script src="/html/canvas/resources/wait-for-canvas-paint.js"></script>
+  <style>
+    .wrapper {
+      width: 400px;
+      height: 120px;
+    }
+    .base::before {
+      content: url("https://{{hosts[alt][www]}}:{{ports[https][0]}}/images/red-100x100.png");
+      width: 100px;
+      height: 100px;
+      background-color: green;
+      display: inline-block;
+    }
+    .base::after {
+      content: url("https://{{hosts[alt][www]}}:{{ports[https][0]}}/images/red-100x100.png");
+      width: 100px;
+      height: 100px;
+      background-color: green;
+      display: inline-block;
+    }
+  </style>
+</head>
+<body>
+  <canvas id=canvas width="100" height="200" layoutsubtree>
+    <div id=child class=wrapper>
+      <p class=base>Before/After</p>
+    </div>
+  </canvas>
+
+  <script>
+    window.onload = () => {
+      promise_test(async function(t) {
+        await waitForCanvasPaint(canvas);
+        const context = canvas.getContext("2d");
+        context.drawElementImage(child, 0, 0);
+
+        // Fetch all pixel data once to avoid multiple slow readbacks.
+        const imgData = context.getImageData(0, 0, canvas.width, canvas.height);
+
+        pixel = _getPixelFromImageData(imgData, 50, 50);
+        assert_array_equals(pixel, [0, 128, 0, 255], "Cross origin ::before should not draw");
+
+        pixel = _getPixelFromImageData(imgData, 250, 50);
+        assert_array_equals(pixel, [0, 128, 0, 255], "Cross origin ::after should not draw");
+      });
+    };
+  </script>
+</body>
+</html>
diff --git a/third_party/blink/web_tests/external/wpt/html/canvas/element/manual/draw-element-image/privacy/pseudos/before-after-images-moved-ignored.tentative.https.sub.html b/third_party/blink/web_tests/external/wpt/html/canvas/element/manual/draw-element-image/privacy/pseudos/before-after-images-moved-ignored.tentative.https.sub.html
new file mode 100644
index 0000000..0eb6f3b
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/html/canvas/element/manual/draw-element-image/privacy/pseudos/before-after-images-moved-ignored.tentative.https.sub.html
@@ -0,0 +1,60 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>drawElementImage does not draw cross-origin images for before/after</title>
+  <script src='/resources/testharness.js'></script>
+  <script src='/resources/testharnessreport.js'></script>
+  <script src='/html/canvas/resources/canvas-tests.js'></script>
+  <script src="/html/canvas/resources/wait-for-canvas-paint.js"></script>
+  <style>
+    .wrapper {
+      width: 400px;
+      height: 120px;
+    }
+    .base::before {
+      content: url("https://{{hosts[alt][www]}}:{{ports[https][0]}}/images/red-100x100.png");
+      width: 100px;
+      height: 100px;
+      background-color: green;
+      display: inline-block;
+    }
+    .base::after {
+      content: url("https://{{hosts[alt][www]}}:{{ports[https][0]}}/images/red-100x100.png");
+      width: 100px;
+      height: 100px;
+      background-color: green;
+      display: inline-block;
+    }
+  </style>
+</head>
+<body>
+  <canvas id=canvas width="100" height="200" layoutsubtree>
+    <div id=child class=wrapper>
+    </div>
+  </canvas>
+  <p id="text" class=base>Before/After</p>
+
+  <script>
+    window.onload = () => {
+      promise_test(async function(t) {
+        await waitForCanvasPaint(canvas);
+
+        child.moveBefore(text, null);
+
+        await waitForCanvasPaint(canvas);
+        const context = canvas.getContext("2d");
+        context.drawElementImage(child, 0, 0);
+
+        // Fetch all pixel data once to avoid multiple slow readbacks.
+        const imgData = context.getImageData(0, 0, canvas.width, canvas.height);
+
+        pixel = _getPixelFromImageData(imgData, 50, 50);
+        assert_array_equals(pixel, [0, 128, 0, 255], "Cross origin ::before should not draw");
+
+        pixel = _getPixelFromImageData(imgData, 250, 50);
+        assert_array_equals(pixel, [0, 128, 0, 255], "Cross origin ::after should not draw");
+      });
+    };
+  </script>
+</body>
+</html>
diff --git a/third_party/blink/web_tests/external/wpt/html/canvas/element/manual/draw-element-image/privacy/pseudos/before-after-visited-color-ignored.tentative.html b/third_party/blink/web_tests/external/wpt/html/canvas/element/manual/draw-element-image/privacy/pseudos/before-after-visited-color-ignored.tentative.html
new file mode 100644
index 0000000..977142ce
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/html/canvas/element/manual/draw-element-image/privacy/pseudos/before-after-visited-color-ignored.tentative.html
@@ -0,0 +1,64 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>drawElementImage does not draw visited colors for before/after</title>
+  <script src='/resources/testharness.js'></script>
+  <script src='/resources/testharnessreport.js'></script>
+  <script src='/html/canvas/resources/canvas-tests.js'></script>
+  <script src="/html/canvas/resources/wait-for-canvas-paint.js"></script>
+  <style>
+    a {
+      color: green;
+    }
+    a:visited {
+      color: red;
+    }
+    .wrapper {
+      width: 400px;
+      height: 120px;
+    }
+    .base::before {
+      content: "";
+      width: 100px;
+      height: 100px;
+      background-color: currentColor;
+      display: inline-block;
+    }
+    .base::after {
+      content: "";
+      width: 100px;
+      height: 100px;
+      background-color: currentColor;
+      display: inline-block;
+    }
+  </style>
+</head>
+<body>
+  <a href="">
+    <canvas id=canvas width="100" height="200" layoutsubtree>
+      <div id=child class=wrapper>
+        <p class=base>Before/After</p>
+      </div>
+    </canvas>
+  </a>
+
+  <script>
+    window.onload = () => {
+      promise_test(async function(t) {
+        await waitForCanvasPaint(canvas);
+        const context = canvas.getContext("2d");
+        context.drawElementImage(child, 0, 0);
+
+        // Fetch all pixel data once to avoid multiple slow readbacks.
+        const imgData = context.getImageData(0, 0, canvas.width, canvas.height);
+
+        pixel = _getPixelFromImageData(imgData, 50, 50);
+        assert_array_equals(pixel, [0, 128, 0, 255], "Visited color in ::before should not draw");
+
+        pixel = _getPixelFromImageData(imgData, 250, 50);
+        assert_array_equals(pixel, [0, 128, 0, 255], "Visited color in ::after should not draw");
+      });
+    };
+  </script>
+</body>
+</html>
diff --git a/third_party/blink/web_tests/external/wpt/html/canvas/element/manual/draw-element-image/privacy/pseudos/before-background-image-iframe-ignored.tentative.https.sub.html b/third_party/blink/web_tests/external/wpt/html/canvas/element/manual/draw-element-image/privacy/pseudos/before-background-image-iframe-ignored.tentative.https.sub.html
new file mode 100644
index 0000000..2f236942
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/html/canvas/element/manual/draw-element-image/privacy/pseudos/before-background-image-iframe-ignored.tentative.https.sub.html
@@ -0,0 +1,39 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>drawElementImage does not draw cross-origin background images for before/after</title>
+  <script src='/resources/testharness.js'></script>
+  <script src='/resources/testharnessreport.js'></script>
+  <script src='/html/canvas/resources/canvas-tests.js'></script>
+  <script src="/html/canvas/resources/wait-for-canvas-paint.js"></script>
+  <style>
+    iframe {
+      width: 100px;
+      height: 200px;
+      background-color: blue;
+    }
+  </style>
+</head>
+<body>
+  <canvas id=canvas width="100" height="200" layoutsubtree>
+    <div id=child>
+      <iframe id=iframe src="../support/subframe-cross-origin-before-background-on-body.https.sub.html"></iframe>
+    </div>
+  </canvas>
+
+  <script>
+    iframe.onload = () => {
+      promise_test(async function(t) {
+        await waitForCanvasPaint(canvas);
+        const context = canvas.getContext("2d");
+        context.drawElementImage(child, 0, 0);
+
+        let pixel = context.getImageData(20, 20, 1, 1).data;
+        assert_false(pixel[0] > 0, "The canvas should not paint cross origin content.");
+        assert_true(pixel[1] > 0, "The canvas should not paint cross origin content.");
+        assert_false(pixel[2] > 0, "The canvas should not paint cross origin content.");
+      });
+    };
+  </script>
+</body>
+</html>
diff --git a/third_party/blink/web_tests/external/wpt/html/canvas/element/manual/draw-element-image/privacy/pseudos/before-background-image-iframe-modified-ignored.tentative.https.sub.html b/third_party/blink/web_tests/external/wpt/html/canvas/element/manual/draw-element-image/privacy/pseudos/before-background-image-iframe-modified-ignored.tentative.https.sub.html
new file mode 100644
index 0000000..4f70984
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/html/canvas/element/manual/draw-element-image/privacy/pseudos/before-background-image-iframe-modified-ignored.tentative.https.sub.html
@@ -0,0 +1,50 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>drawElementImage does not draw cross-origin background images for before/after when modified</title>
+  <script src='/resources/testharness.js'></script>
+  <script src='/resources/testharnessreport.js'></script>
+  <script src='/html/canvas/resources/canvas-tests.js'></script>
+  <script src="/html/canvas/resources/wait-for-canvas-paint.js"></script>
+  <style>
+    iframe {
+      width: 100px;
+      height: 200px;
+      background-color: blue;
+    }
+  </style>
+</head>
+<body>
+  <canvas id=canvas width="100" height="200" layoutsubtree>
+    <div id=child>
+      <iframe id=iframe src="../support/subframe-cross-origin-before-background-on-body-modified.https.sub.html"></iframe>
+    </div>
+  </canvas>
+
+  <script>
+    iframe.onload = () => {
+      promise_test(async function(t) {
+        await waitForCanvasPaint(canvas);
+        const context = canvas.getContext("2d");
+        context.drawElementImage(child, 0, 0);
+
+        let pixel = context.getImageData(20, 20, 1, 1).data;
+        assert_false(pixel[0] > 0, "Initial draw: Red should not paint.");
+        assert_false(pixel[1] > 0, "Initial draw: Green should not paint before class is added.");
+        assert_true(pixel[2] > 0, "Initial draw: Blue iframe background should paint.");
+
+        // Add class to iframe body causing it to match the ::before rule.
+        iframe.contentDocument.body.classList.add('has-before');
+
+        await waitForCanvasPaint(canvas);
+        context.drawElementImage(child, 0, 0);
+
+        pixel = context.getImageData(20, 20, 1, 1).data;
+        assert_false(pixel[0] > 0, "The canvas should not paint cross origin content.");
+        assert_true(pixel[1] > 0, "The canvas should paint the ::before background color.");
+        assert_false(pixel[2] > 0, "The canvas should paint the ::before element over the iframe background.");
+      });
+    };
+  </script>
+</body>
+</html>
diff --git a/third_party/blink/web_tests/external/wpt/html/canvas/element/manual/draw-element-image/privacy/pseudos/before-background-image-iframe-moved-ignored.tentative.https.sub.html b/third_party/blink/web_tests/external/wpt/html/canvas/element/manual/draw-element-image/privacy/pseudos/before-background-image-iframe-moved-ignored.tentative.https.sub.html
... (truncated)
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.