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 <[email protected]> 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 <[email protected]> Reviewed-by: Stefan Zager <[email protected]> Commit-Queue: Philip Rogers <[email protected]> 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