High watch CVSS 4.3 chrome Info Disclosure 🔧 Commit mapped

Overview

High
Severity
4.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInformation leak in Canvas
DescriptionInformation leak in Canvas
ComponentCanvas
Sub-componentHiC
Bug ClassInfo Disclosure
Action tierwatch — information disclosure — security-relevant, below the RCE-class bar
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker532904047
Fix commit584ed97cc815 (chromium/src) +1221/-19
CWECWE-200 (Information exposure)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:N/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-07-09 (per vendor advisory)
Fix landed2026-08-18 (public git)
Disclosed2026-08-25
Patch gap7 days — fix public in source before the advisory

Background

HTML-in-Canvas (`[HiC]`)
An experimental Blink feature in which a <canvas layoutsubtree> lays out its DOM children and lets script rasterize them into the canvas with drawElementImage(). The canvas pixels can then be read back with getImageData(), as the new .tentative WPTs do.
`IsInCanvasSubtree()`
A per-Element flag set for content under such a canvas. Paint uses it to apply privacy protections: in the new tests, cross-origin images and :visited link colors are not drawn for flagged content.
Pseudo elements
PseudoElement objects (::before, ::after, ::marker, ::first-letter, ::details-content, etc.) are stored in the originating element’s NodeRareData (GetPseudoElements()), not as ordinary DOM children, so ElementTraversal::ChildrenOf() does not visit them.
Paint invalidation
When the flag changes, DidChangeIsInCanvasSubtree() must make sure cached display items are repainted, or stale unprotected paint output may be reused.

Root Cause Analysis

The flag-propagation routine in element.cc (the hunk shows the value == IsInCanvasSubtree() early-out and the recursive child.SetIsInCanvasSubtree(value), i.e. Element::SetIsInCanvasSubtree) walked shadow roots, slot-assigned nodes, frame-owner documents and DOM children, but never the element’s pseudo elements.

As a result, ::before/::after/::marker and similar pseudos of an element inside a layoutsubtree canvas kept IsInCanvasSubtree() == false. They were painted without the canvas privacy filtering, so cross-origin content: url(...) images, :visited-dependent currentColor, and similar private content were rasterized by drawElementImage() and became readable through getImageData().

The fix adds a loop over RareData()->GetPseudoElements() that calls SetIsInCanvasSubtree(value) on each pseudo. It also changes DidChangeIsInCanvasSubtree() to use SlowSetPaintingLayerNeedsRepaintAndInvalidateDisplayItemClient(), so the enclosing painting layer is repainted even when the object has no layer of its own, which covers the “moved” tests that use moveBefore(). The DCHECK-only verifier was converted to a member and extended to check pseudos too.

Key insight
Privacy-relevant state was propagated only through DOM, shadow, slot and frame edges, and pseudo elements live outside those edges. Propagating IsInCanvasSubtree to every PseudoElement, and forcing a repaint of the painting layer when it changes, closes the gap.

Attack Path

  1. Canvas with layout subtree The attacker page places an element under <canvas layoutsubtree>. This requires the HTML-in-Canvas feature to be enabled; its tests are .tentative, so it is likely not enabled by default for ordinary web content (inferred).
  2. Private content via pseudo elements The element’s ::before/::after/::marker/::first-line and similar pseudos display a cross-origin image (content: url(...), background images, or content in a cross-origin iframe) or a :visited-dependent currentColor. The element can be placed there directly or moved in with moveBefore().
  3. Unprotected paint Before the fix, those pseudos lacked IsInCanvasSubtree, so paint drew the real cross-origin pixels and the visited color into the canvas.
  4. Read back Script calls drawElementImage() and then getImageData() to read the pixels. This leaks cross-origin image content or browsing-history (visited-link) state, as the new tests’ assertions encode.

Impact Assessment

A web page that can use HTML-in-Canvas could read cross-origin image pixels and learn whether URLs were visited. This is an information leak within the renderer that breaks the same-origin and :visited privacy protections, as the advisory says (“Information leak in Canvas”). No memory corruption is involved, and the patch shows no escalation beyond disclosure. Reachability depends on the HTML-in-Canvas feature being enabled. The vendor advisory rates this high severity and does not report in-the-wild exploitation; NVD scores it CVSS 4.3.

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
promise_test
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
modified
promise_test
third_party/blink/web_tests/external/wpt/html/canvas/element/manual/draw-element-image/privacy/pseudos/before-after-visited-color-ignored.tentative.html
modified
promise_test
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
modified

Files Changed

  • third_party/blink/web_tests/external/wpt/html/canvas/element/manual/draw-element-image/privacy/pseudos/details-content-visited-link-ignored.tentative.html
  • 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
  • third_party/blink/web_tests/external/wpt/html/canvas/element/manual/draw-element-image/privacy/visited-link/visited-link-svg-use-outside-subtree-ignored.tentative.html
  • third_party/blink/web_tests/external/wpt/html/canvas/element/manual/draw-element-image/privacy/pseudos/first-line-visited-link-color-moved-ignored.tentative.html
  • third_party/blink/web_tests/external/wpt/html/canvas/element/manual/draw-element-image/privacy/pseudos/first-line-visited-link-currentcolor-ignored.tentative.html
  • 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
  • third_party/blink/web_tests/external/wpt/html/canvas/element/manual/draw-element-image/privacy/pseudos/first-letter-background-images-ignored.tentative.https.sub.html
  • third_party/blink/web_tests/external/wpt/html/canvas/element/manual/draw-element-image/privacy/pseudos/first-line-visited-link-color-ignored.tentative.html
  • third_party/blink/web_tests/external/wpt/html/canvas/element/manual/draw-element-image/privacy/pseudos/before-after-images-ignored.tentative.https.sub.html
  • third_party/blink/web_tests/external/wpt/html/canvas/element/manual/draw-element-image/privacy/pseudos/details-content-after-image-ignored.tentative.https.sub.html
  • third_party/blink/web_tests/external/wpt/html/canvas/element/manual/draw-element-image/privacy/pseudos/before-images-iframe-ignored.tentative.https.sub.html
  • third_party/blink/web_tests/external/wpt/html/canvas/element/manual/draw-element-image/privacy/visited-link/visited-link-slotted-color-ignored.tentative.html
  • 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/support/subframe-cross-origin-before-content-on-body.https.sub.html
  • third_party/blink/web_tests/external/wpt/html/canvas/element/manual/draw-element-image/privacy/pseudos/marker-images-ignored.tentative.https.sub.html
  • third_party/blink/web_tests/external/wpt/html/canvas/element/manual/draw-element-image/privacy/pseudos/before-images-iframe-moved-ignored.tentative.https.sub.html
  • third_party/blink/web_tests/external/wpt/html/canvas/element/manual/draw-element-image/privacy/pseudos/first-line-background-images-ignored.tentative.https.sub.html
  • third_party/blink/web_tests/external/wpt/html/canvas/element/manual/draw-element-image/privacy/pseudos/first-line-background-images-moved-ignored.tentative.https.sub.html
  • third_party/blink/web_tests/external/wpt/html/canvas/element/manual/draw-element-image/privacy/pseudos/before-after-visited-color-ignored.tentative.html
  • third_party/blink/renderer/core/dom/element.h
  • third_party/blink/web_tests/external/wpt/html/canvas/element/manual/draw-element-image/privacy/support/subframe-cross-origin-before-background-on-body-modified.https.sub.html
  • third_party/blink/renderer/core/dom/element.cc
  • third_party/blink/web_tests/external/wpt/html/canvas/element/manual/draw-element-image/privacy/pseudos/first-letter-visited-link-currentcolor-ignored.tentative.html
  • third_party/blink/web_tests/external/wpt/html/canvas/element/manual/draw-element-image/privacy/pseudos/details-content-background-image-ignored.tentative.https.sub.html
  • 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
  • third_party/blink/web_tests/external/wpt/html/canvas/element/manual/draw-element-image/privacy/pseudos/marker-visited-link-currentcolor-ignored.tentative.html
  • third_party/blink/web_tests/external/wpt/html/canvas/element/manual/draw-element-image/privacy/support/subframe-cross-origin-before-background-on-body.https.sub.html
  • third_party/blink/web_tests/external/wpt/html/canvas/element/manual/draw-element-image/privacy/pseudos/details-content-visited-link-currentcolor-ignored.tentative.html
  • third_party/blink/web_tests/external/wpt/html/canvas/element/manual/draw-element-image/privacy/pseudos/first-letter-visited-link-color-ignored.tentative.html
  • 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

Audit Directions

  • Tree walks that skip pseudo elements
    Audit other flag-propagation and invalidation walks (inertness, content-visibility, privacy and taint flags) that use ElementTraversal::ChildrenOf() and may miss PseudoElements, including nested pseudos like ::details-content::after.
  • Remaining pseudo types and rendering paths
    The commit says more pseudos still need testing. Check ::first-line style inheritance, ::marker, ::backdrop, ::selection, ::placeholder, view-transition pseudos, and SVG <use> shadow content under drawElementImage().
  • Flag changes without full repaint
    Look for privacy-relevant paint flags whose change handlers only invalidate the object’s own layer. Moving content in or out of a protected subtree (moveBefore(), slotting, iframe changes) can leave cached unprotected display items behind.
From 584ed97cc8158833992d052aa92b0cd0b9eff8f5 Mon Sep 17 00:00:00 2001
From: Stephen Chenney <[email protected]>
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 <[email protected]>
Commit-Queue: Stephen Chenney <[email protected]>
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);
+
+        // 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 @@
... (file diff truncated)
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.