Chrome · Paint
CVE-2026-11139
Logic Error in Paint
Overview
Medium
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
promise_testthird_party/blink/web_tests/wpt_internal/html/canvas/drawElementImage/privacy/box-reflect-images-ignored.https.sub.html |
modified |
Files Changed
third_party/blink/renderer/core/paint/paint_layer.ccthird_party/blink/web_tests/wpt_internal/html/canvas/drawElementImage/privacy/box-reflect-images-ignored.https.sub.htmlthird_party/blink/web_tests/wpt_internal/html/canvas/drawElementImage/resources/left-half-mask-50.svg
Patch
From 941de2f7b1394f3395b0b1a834b6c4c3bf1aefbf Mon Sep 17 00:00:00 2001
From: Philip Rogers <pdr@chromium.org>
Date: Fri, 17 Apr 2026 12:37:28 -0700
Subject: [PATCH] [html-in-canvas] Fix webkit-box-reflect privacy-preserving painting
-webkit-box-reflect accepts an image, and we were not honoring
privacy-preserving painting of that image. This patch strips
cross-origin box reflect images when under canvas.
Fixed: 501650594
Change-Id: Ia50cc4cf12a24426edd82cf2d32296f862b6b621
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7751504
Auto-Submit: Philip Rogers <pdr@chromium.org>
Reviewed-by: Stefan Zager <szager@chromium.org>
Commit-Queue: Philip Rogers <pdr@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1616780}
---
diff --git a/third_party/blink/renderer/core/paint/paint_layer.cc b/third_party/blink/renderer/core/paint/paint_layer.cc
index 4566099..921e5e0 100644
--- a/third_party/blink/renderer/core/paint/paint_layer.cc
+++ b/third_party/blink/renderer/core/paint/paint_layer.cc
@@ -2453,6 +2453,24 @@
FilterOperations filter_operations = style.Filter();
if (GetLayoutObject().HasReflection() && GetLayoutObject().IsBox()) {
BoxReflection reflection = BoxReflectionForPaintLayer(*this, style);
+
+ if (RuntimeEnabledFeatures::CanvasDrawElementEnabled(
+ GetLayoutObject().GetDocument().GetExecutionContext())) {
+ auto* element = DynamicTo<Element>(GetLayoutObject().GetNode());
+ if (element && element->IsInCanvasSubtree()) {
+ if (const auto* reflect_style = style.BoxReflect()) {
+ if (auto* style_image = reflect_style->Mask().GetImage()) {
+ // Strip the mask image if it is being rendered into a canvas and it
+ // is cross-origin.
+ if (!style_image->IsCorsSameOrigin()) {
+ reflection =
+ BoxReflection(reflection.Direction(), reflection.Offset());
+ }
+ }
+ }
+ }
+ }
+
filter_operations.Operations().push_back(
MakeGarbageCollected<BoxReflectFilterOperation>(reflection));
}
diff --git a/third_party/blink/web_tests/wpt_internal/html/canvas/drawElementImage/privacy/box-reflect-images-ignored.https.sub.html b/third_party/blink/web_tests/wpt_internal/html/canvas/drawElementImage/privacy/box-reflect-images-ignored.https.sub.html
new file mode 100644
index 0000000..be75e3c
--- /dev/null
+++ b/third_party/blink/web_tests/wpt_internal/html/canvas/drawElementImage/privacy/box-reflect-images-ignored.https.sub.html
@@ -0,0 +1,86 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>drawElementImage does not draw cross-origin box-reflect images</title>
+ <link rel="help" href="https://github.com/WICG/html-in-canvas">
+ <script src='/resources/testharness.js'></script>
+ <script src='/resources/testharnessreport.js'></script>
+ <style>
+ #child {
+ width: 200px;
+ height: 200px;
+ background: blue;
+ position: relative;
+ }
+ #sameOrigin {
+ position: absolute;
+ left: 0px;
+ top: 0px;
+ width: 100px;
+ height: 100px;
+ background:green;
+ -webkit-box-reflect: below 0 url("https://{{location[host]}}/wpt_internal/html/canvas/drawElementImage/resources/left-half-mask-50.svg");
+ }
+ #crossOrigin {
+ position: absolute;
+ left: 100px;
+ top: 0px;
+ width: 100px;
+ height: 100px;
+ background: green;
+ -webkit-box-reflect: below 0 url("https://{{hosts[alt][www]}}:{{ports[h2][0]}}/wpt_internal/html/canvas/drawElementImage/resources/left-half-mask-50.svg");
+ }
+ </style>
+</head>
+<body>
+ <canvas id="canvas" width="200" height="200" layoutsubtree>
+ <!--
+ #child renders a 200x200 blue rect. The left side #sameOrigin and its
+ reflection. The right side is #crossOrigin and its reflection.
+ -->
+ <div id="child">
+ <div id="sameOrigin"></div>
+ <div id="crossOrigin"></div>
+ </div>
+ </canvas>
+
+ <script>
+ window.onload = () => {
+ promise_test(async function(t) {
+ await new Promise(requestAnimationFrame);
+ await new Promise(setTimeout);
+ var 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).data;
+
+ // Helper function to extract a pixel's RGBA array at (x, y).
+ const getPixel = (x, y) => {
+ const index = (y * canvas.width + x) * 4;
+ return [
+ imgData[index],
+ imgData[index + 1],
+ imgData[index + 2],
+ imgData[index + 3]
+ ];
+ };
+
+ let pixel = getPixel(50, 50);
+ assert_array_equals(pixel, [0, 128, 0, 255], "Same origin div should draw");
+ pixel = getPixel(25, 150);
+ assert_array_equals(pixel, [0, 128, 0, 255], "Same origin box-reflect mask should not mask out left side of reflection");
+ pixel = getPixel(75, 150);
+ assert_array_equals(pixel, [0, 0, 255, 255], "Same origin box-reflect mask should mask out right side of reflection");
+
+ pixel = getPixel(150, 50);
+ assert_array_equals(pixel, [0, 128, 0, 255], "Cross origin div should draw");
+ pixel = getPixel(125, 150);
+ assert_array_equals(pixel, [0, 128, 0, 255], "Cross origin box-reflect mask should not mask out left side of reflection");
+ pixel = getPixel(175, 150);
+ assert_array_equals(pixel, [0, 128, 0, 255], "Cross origin box-reflect mask should not mask out right side of reflection");
+ });
+ };
+ </script>
+</body>
+</html>
diff --git a/third_party/blink/web_tests/wpt_internal/html/canvas/drawElementImage/resources/left-half-mask-50.svg b/third_party/blink/web_tests/wpt_internal/html/canvas/drawElementImage/resources/left-half-mask-50.svg
new file mode 100644
index 0000000..4268264
--- /dev/null
+++ b/third_party/blink/web_tests/wpt_internal/html/canvas/drawElementImage/resources/left-half-mask-50.svg
@@ -0,0 +1,4 @@
+<svg xmlns='http://www.w3.org/2000/svg' viewBox="0 0 100 100">
+ <rect width='50%' height='100%' fill='black'/>
+ <!-- The right half is left transparent. -->
+</svg>
Loading diff…
Regression Test / PoC
shipped with the fix
diff --git a/third_party/blink/web_tests/wpt_internal/html/canvas/drawElementImage/privacy/box-reflect-images-ignored.https.sub.html b/third_party/blink/web_tests/wpt_internal/html/canvas/drawElementImage/privacy/box-reflect-images-ignored.https.sub.html
new file mode 100644
index 0000000..be75e3c
--- /dev/null
+++ b/third_party/blink/web_tests/wpt_internal/html/canvas/drawElementImage/privacy/box-reflect-images-ignored.https.sub.html
@@ -0,0 +1,86 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>drawElementImage does not draw cross-origin box-reflect images</title>
+ <link rel="help" href="https://github.com/WICG/html-in-canvas">
+ <script src='/resources/testharness.js'></script>
+ <script src='/resources/testharnessreport.js'></script>
+ <style>
+ #child {
+ width: 200px;
+ height: 200px;
+ background: blue;
+ position: relative;
+ }
+ #sameOrigin {
+ position: absolute;
+ left: 0px;
+ top: 0px;
+ width: 100px;
+ height: 100px;
+ background:green;
+ -webkit-box-reflect: below 0 url("https://{{location[host]}}/wpt_internal/html/canvas/drawElementImage/resources/left-half-mask-50.svg");
+ }
+ #crossOrigin {
+ position: absolute;
+ left: 100px;
+ top: 0px;
+ width: 100px;
+ height: 100px;
+ background: green;
+ -webkit-box-reflect: below 0 url("https://{{hosts[alt][www]}}:{{ports[h2][0]}}/wpt_internal/html/canvas/drawElementImage/resources/left-half-mask-50.svg");
+ }
+ </style>
+</head>
+<body>
+ <canvas id="canvas" width="200" height="200" layoutsubtree>
+ <!--
+ #child renders a 200x200 blue rect. The left side #sameOrigin and its
+ reflection. The right side is #crossOrigin and its reflection.
+ -->
+ <div id="child">
+ <div id="sameOrigin"></div>
+ <div id="crossOrigin"></div>
+ </div>
+ </canvas>
+
+ <script>
+ window.onload = () => {
+ promise_test(async function(t) {
+ await new Promise(requestAnimationFrame);
+ await new Promise(setTimeout);
+ var 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).data;
+
+ // Helper function to extract a pixel's RGBA array at (x, y).
+ const getPixel = (x, y) => {
+ const index = (y * canvas.width + x) * 4;
+ return [
+ imgData[index],
+ imgData[index + 1],
+ imgData[index + 2],
+ imgData[index + 3]
+ ];
+ };
+
+ let pixel = getPixel(50, 50);
+ assert_array_equals(pixel, [0, 128, 0, 255], "Same origin div should draw");
+ pixel = getPixel(25, 150);
+ assert_array_equals(pixel, [0, 128, 0, 255], "Same origin box-reflect mask should not mask out left side of reflection");
+ pixel = getPixel(75, 150);
+ assert_array_equals(pixel, [0, 0, 255, 255], "Same origin box-reflect mask should mask out right side of reflection");
+
+ pixel = getPixel(150, 50);
+ assert_array_equals(pixel, [0, 128, 0, 255], "Cross origin div should draw");
+ pixel = getPixel(125, 150);
+ assert_array_equals(pixel, [0, 128, 0, 255], "Cross origin box-reflect mask should not mask out left side of reflection");
+ pixel = getPixel(175, 150);
+ assert_array_equals(pixel, [0, 128, 0, 255], "Cross origin box-reflect mask should not mask out right side of reflection");
+ });
+ };
+ </script>
+</body>
+</html>
diff --git a/third_party/blink/web_tests/wpt_internal/html/canvas/drawElementImage/resources/left-half-mask-50.svg b/third_party/blink/web_tests/wpt_internal/html/canvas/drawElementImage/resources/left-half-mask-50.svg
new file mode 100644
index 0000000..4268264
--- /dev/null
+++ b/third_party/blink/web_tests/wpt_internal/html/canvas/drawElementImage/resources/left-half-mask-50.svg
@@ -0,0 +1,4 @@
+<svg xmlns='http://www.w3.org/2000/svg' viewBox="0 0 100 100">
+ <rect width='50%' height='100%' fill='black'/>
+ <!-- The right half is left transparent. -->
+</svg>
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.
References
On This Page