CVE-2026-79193
Overview
Files Changed
third_party/blink/renderer/modules/canvas/canvas2d/canvas_2d_recorder_context.ccthird_party/blink/web_tests/external/wpt/css/css-images/image-orientation/image-orientation-none-cross-origin-canvas-css-image-value.sub.htmlthird_party/blink/web_tests/external/wpt/css/css-images/image-orientation/reference/image-orientation-none-cross-origin-canvas-css-image-value-ref.sub.html
Patch
From 919d6580fccb08492b4822b32403bb7e78a77cab Mon Sep 17 00:00:00 2001 From: Kevin Babbitt <[email protected]> Date: Fri, 17 Jul 2026 09:34:50 -0700 Subject: [PATCH] Canvas2D: Respect EXIF orientation for cross-origin CSSImageValue Enforce kRespectImageOrientation for any source that would taint the origin, including CSS image values. The IsImageBitmap() || IsImageElement() checks which previously gated the WouldTaintOrigin() check had been introduced to fix a CHECK failure in SVGImage::CheckLoaded(); see https://crrev.com/c/2521049. However, in https://crrev.com/c/5892795 we removed the call to CheckLoaded() on this path, so the image type gates are no longer needed. Fixed: 517655953 Change-Id: I1e1838c3af56264fb49c4086c9cf6ae28fe84d15 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8102890 Commit-Queue: Kevin Babbitt <[email protected]> Reviewed-by: Colin Blundell <[email protected]> Cr-Commit-Position: refs/heads/main@{#1663972} --- diff --git a/third_party/blink/renderer/modules/canvas/canvas2d/canvas_2d_recorder_context.cc b/third_party/blink/renderer/modules/canvas/canvas2d/canvas_2d_recorder_context.cc index ddda6d1e..133ae40 100644 --- a/third_party/blink/renderer/modules/canvas/canvas2d/canvas_2d_recorder_context.cc +++ b/third_party/blink/renderer/modules/canvas/canvas2d/canvas_2d_recorder_context.cc @@ -855,8 +855,7 @@ RespectImageOrientationEnum Canvas2DRecorderContext::RespectImageOrientationInternal( CanvasImageSource* image_source) { - if ((image_source->IsImageBitmap() || image_source->IsImageElement()) && - image_source->WouldTaintOrigin()) { + if (image_source->WouldTaintOrigin()) { return kRespectImageOrientation; } return RespectImageOrientation(); diff --git a/third_party/blink/web_tests/external/wpt/css/css-images/image-orientation/image-orientation-none-cross-origin-canvas-css-image-value.sub.html b/third_party/blink/web_tests/external/wpt/css/css-images/image-orientation/image-orientation-none-cross-origin-canvas-css-image-value.sub.html new file mode 100644 index 0000000..4a42203 --- /dev/null +++ b/third_party/blink/web_tests/external/wpt/css/css-images/image-orientation/image-orientation-none-cross-origin-canvas-css-image-value.sub.html @@ -0,0 +1,36 @@ +<!DOCTYPE html> +<html class="reftest-wait"> +<head> +<meta charset="utf-8"> +<title>CSS Images Module Level 3: image-orientation: none for a cross-origin CSSImageValue drawn to canvas</title> +<link rel="help" href="https://github.com/w3c/csswg-drafts/issues/5165"> +<link rel="match" href="reference/image-orientation-none-cross-origin-canvas-css-image-value-ref.sub.html"> +<style> + #target { image-orientation: none; } + #loader { + width: 0; + height: 0; + background-image: url("http://{{hosts[][]}}:{{ports[http][1]}}/css/css-images/image-orientation/support/exif-orientation-6-ru.jpg"); + } +</style> +</head> +<body> + <canvas id="target" width="100" height="50"></canvas> + <div id="loader"></div> + <img id="probe" style="display:none"> +</body> +<script> + const probe = document.getElementById('probe'); + probe.onload = () => { + requestAnimationFrame(() => requestAnimationFrame(() => { + const loader = document.getElementById('loader'); + const cssImage = loader.computedStyleMap().get('background-image'); + const ctx = document.getElementById('target').getContext('2d'); + ctx.drawImage(cssImage, 0, 0); + document.documentElement.classList.remove('reftest-wait'); + })); + }; + probe.src = getComputedStyle(document.getElementById('loader')) + .backgroundImage.slice(5, -2); +</script> +</html> diff --git a/third_party/blink/web_tests/external/wpt/css/css-images/image-orientation/reference/image-orientation-none-cross-origin-canvas-css-image-value-ref.sub.html b/third_party/blink/web_tests/external/wpt/css/css-images/image-orientation/reference/image-orientation-none-cross-origin-canvas-css-image-value-ref.sub.html new file mode 100644 index 0000000..b99292f --- /dev/null +++ b/third_party/blink/web_tests/external/wpt/css/css-images/image-orientation/reference/image-orientation-none-cross-origin-canvas-css-image-value-ref.sub.html @@ -0,0 +1,26 @@ +<!DOCTYPE html> +<html class="reftest-wait"> +<head> +<meta charset="utf-8"> +<title>CSS Images Module Level 3: image-orientation: none for a cross-origin CSSImageValue drawn to canvas (reference)</title> +<style> + #target { image-orientation: none; } +</style> +</head> +<body> + <canvas id="target" width="100" height="50"></canvas> + <div id="loader"></div> + <img id="probe" style="display:none"> +</body> +<script> + const probe = document.getElementById('probe'); + probe.onload = () => { + requestAnimationFrame(() => requestAnimationFrame(() => { + const ctx = document.getElementById('target').getContext('2d'); + ctx.drawImage(probe, 0, 0); + document.documentElement.classList.remove('reftest-wait'); + })); + }; + probe.src = "http://{{hosts[][]}}:{{ports[http][1]}}/css/css-images/image-orientation/support/exif-orientation-6-ru.jpg"; +</script> +</html>
Regression Test / PoC
diff --git a/third_party/blink/web_tests/external/wpt/css/css-images/image-orientation/image-orientation-none-cross-origin-canvas-css-image-value.sub.html b/third_party/blink/web_tests/external/wpt/css/css-images/image-orientation/image-orientation-none-cross-origin-canvas-css-image-value.sub.html
new file mode 100644
index 0000000..4a42203
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/css/css-images/image-orientation/image-orientation-none-cross-origin-canvas-css-image-value.sub.html
@@ -0,0 +1,36 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<head>
+<meta charset="utf-8">
+<title>CSS Images Module Level 3: image-orientation: none for a cross-origin CSSImageValue drawn to canvas</title>
+<link rel="help" href="https://github.com/w3c/csswg-drafts/issues/5165">
+<link rel="match" href="reference/image-orientation-none-cross-origin-canvas-css-image-value-ref.sub.html">
+<style>
+ #target { image-orientation: none; }
+ #loader {
+ width: 0;
+ height: 0;
+ background-image: url("http://{{hosts[][]}}:{{ports[http][1]}}/css/css-images/image-orientation/support/exif-orientation-6-ru.jpg");
+ }
+</style>
+</head>
+<body>
+ <canvas id="target" width="100" height="50"></canvas>
+ <div id="loader"></div>
+ <img id="probe" style="display:none">
+</body>
+<script>
+ const probe = document.getElementById('probe');
+ probe.onload = () => {
+ requestAnimationFrame(() => requestAnimationFrame(() => {
+ const loader = document.getElementById('loader');
+ const cssImage = loader.computedStyleMap().get('background-image');
+ const ctx = document.getElementById('target').getContext('2d');
+ ctx.drawImage(cssImage, 0, 0);
+ document.documentElement.classList.remove('reftest-wait');
+ }));
+ };
+ probe.src = getComputedStyle(document.getElementById('loader'))
+ .backgroundImage.slice(5, -2);
+</script>
+</html>
diff --git a/third_party/blink/web_tests/external/wpt/css/css-images/image-orientation/reference/image-orientation-none-cross-origin-canvas-css-image-value-ref.sub.html b/third_party/blink/web_tests/external/wpt/css/css-images/image-orientation/reference/image-orientation-none-cross-origin-canvas-css-image-value-ref.sub.html
new file mode 100644
index 0000000..b99292f
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/css/css-images/image-orientation/reference/image-orientation-none-cross-origin-canvas-css-image-value-ref.sub.html
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<head>
+<meta charset="utf-8">
+<title>CSS Images Module Level 3: image-orientation: none for a cross-origin CSSImageValue drawn to canvas (reference)</title>
+<style>
+ #target { image-orientation: none; }
+</style>
+</head>
+<body>
+ <canvas id="target" width="100" height="50"></canvas>
+ <div id="loader"></div>
+ <img id="probe" style="display:none">
+</body>
+<script>
+ const probe = document.getElementById('probe');
+ probe.onload = () => {
+ requestAnimationFrame(() => requestAnimationFrame(() => {
+ const ctx = document.getElementById('target').getContext('2d');
+ ctx.drawImage(probe, 0, 0);
+ document.documentElement.classList.remove('reftest-wait');
+ }));
+ };
+ probe.src = "http://{{hosts[][]}}:{{ports[http][1]}}/css/css-images/image-orientation/support/exif-orientation-6-ru.jpg";
+</script>
+</html>
Original Bug Report
Canvas2D drawImage(CSSImageValue) bypasses cross-origin EXIF-orientation clamp
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: A potential vulnerability in Canvas2D allows cross-origin EXIF orientation tags to be leaked via a paint-timing side channel. When drawing a cross-origin, no-CORS image using a CSSImageValue source onto a canvas with image-orientation: none, Blink fails to clamp the EXIF orientation. This allows an attacker to potentially distinguish whether an image has been rotated and leak its orientation tag.
Affected files:
third_party/blink/renderer/modules/canvas/canvas2d/canvas_2d_recorder_context.ccthird_party/blink/renderer/core/css/cssom/css_style_image_value.h
Estimated timestamp from git blame: 2020-11-06
Description
To prevent leaking metadata of cross-origin, opaque (no-CORS) images, the browser must enforce standard image orientation (kRespectImageOrientation) regardless of CSS styling (such as image-orientation: none).
In Canvas2D rendering, Canvas2DRecorderContext::RespectImageOrientationInternal is responsible for overriding the image orientation behavior for cross-origin sources to prevent orientation leaks.
Root Cause
The implementation of RespectImageOrientationInternal uses a hard-coded closed list of source types (IsImageBitmap and IsImageElement) and completely omits CSSStyleImageValue:
// File: third_party/blink/renderer/modules/canvas/canvas2d/canvas_2d_recorder_context.cc
RespectImageOrientationEnum
Canvas2DRecorderContext::RespectImageOrientationInternal(
CanvasImageSource* image_source) {
if ((image_source->IsImageBitmap() || image_source->IsImageElement()) &&
image_source->WouldTaintOrigin()) {
return kRespectImageOrientation;
}
return RespectImageOrientation();
}
CSSStyleImageValue (the underlying C++ implementation for the CSSImageValue IDL interface) implements CanvasImageSource, but inherits the default implementations of IsImageElement() and IsImageBitmap() which return false (defined in third_party/blink/renderer/core/html/canvas/canvas_image_source.h).
Consequently, when drawing a CSSStyleImageValue source, the override condition evaluates to (false || false) && true, which is false. Blink then falls back to RespectImageOrientation(), which respects the canvas element’s author-controlled image-orientation CSS style property. This allows raw, unrotated pixels to be drawn to the canvas when image-orientation: none is set.
While drawing taints the canvas origin and blocks read-back APIs like getImageData, the differential between the unrotated raw image paint and standard orientation paints is observable via timing-based paint side channels.
Potential Attack Vector / Trigger Path
Note: These are potential steps as our tooling does not yet have the ability to run proof-of-concept code.
- Load Target Image: An attacker loads a cross-origin, no-CORS image (e.g., via background-image CSS style). Once loaded, the attacker fetches its
CSSImageValuerepresentation using the Typed OMcomputedStyleMap().get('background-image')API. - Set Orientation to None: The attacker sets
imageOrientation = 'none'on a canvas element. - Draw CSSImageValue: The attacker draws the
CSSImageValueonto the canvas viactx.drawImage(cssImageValue, 0, 0). Because the source is not recognized as restricted, the clamp is bypassed and the raw unrotated decode is drawn. - Measure Difference: By comparing this canvas to a canvas drawn with default orientation using a paint-timing side channel (e.g., stacking both with
mix-blend-mode: differenceunder an expensive SVG filter and measuring rendering times viarequestAnimationFrame), the EXIF orientation tag of the target image is leaked.
Suggested Fix
Update RespectImageOrientationInternal to include CSSStyleImageValue in the check, or more simply, enforce kRespectImageOrientation for any cross-origin resource that can taint the origin and has orientation properties. For example:
RespectImageOrientationEnum
Canvas2DRecorderContext::RespectImageOrientationInternal(
CanvasImageSource* image_source) {
if (image_source->WouldTaintOrigin() &&
!image_source->IsCanvasElement() &&
!image_source->IsOffscreenCanvas()) {
return kRespectImageOrientation;
}
return RespectImageOrientation();
}
Evaluated with Chrome root at commit: 5133b93d189b383c37805b1cf3a9d2dbfe8d7379
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.