CVE-2026-79193
Overview
Background
- EXIF orientation
- Metadata embedded in an image (e.g. a JPEG) that instructs a renderer to rotate/flip the decoded pixels; whether it is honored is controlled by a
RespectImageOrientationEnumvalue (kRespectImageOrientationvs.kDoNotRespectImageOrientation). - `WouldTaintOrigin()`
- A
CanvasImageSourcepredicate that returns true when drawing the source onto a canvas would taint the canvas because the pixel data is cross-origin and not readable by the page. - `CSSImageValue`
- An image sourced from CSS (e.g. a
background-imageobtained viacomputedStyleMap()), one of severalCanvasImageSourcetypes that can be passed todrawImage. - Tainted canvas
- A canvas whose pixel data has been contaminated by cross-origin content; the security model forbids reading it back (
getImageData,toDataURL) precisely to prevent cross-origin pixel disclosure.
Root Cause Analysis
Canvas2DRecorderContext::RespectImageOrientationInternal decides whether to force kRespectImageOrientation for a given CanvasImageSource. The pre-patch condition forced orientation only when the source WouldTaintOrigin() and was specifically an IsImageBitmap() or IsImageElement(); for any other cross-origin taint-inducing source — notably a CSSImageValue — the type gate was false, so the page’s own image-orientation: none was honored instead of the image’s real EXIF orientation. Because the same cross-origin image is rendered with its true EXIF orientation in a same-origin/reference path but with orientation suppressed on the CSS-image canvas path, the dimensions/shape of the drawn result differ in a way observable to the page even though direct pixel readback is blocked, leaking cross-origin information (e.g. whether the image is portrait vs. landscape after EXIF rotation).
The fix removes the IsImageBitmap() || IsImageElement() type gate so that any source that WouldTaintOrigin() forces kRespectImageOrientation, closing the discrepancy. The commit message notes the type gates existed only to avoid a SVGImage::CheckLoaded() CHECK failure that a later change (crrev.com/c/5892795) already eliminated, so the gate was now purely harmful.
IsImageBitmap/IsImageElement) rather than on the security-relevant condition alone; the fix keys the behavior solely on WouldTaintOrigin(), so cross-origin CSS images are handled like every other tainting source.Attack Path
- Load a cross-origin EXIF image via CSS
The attacker page sets a
background-imageto a cross-origin JPEG carrying an EXIF orientation (e.g. orientation 6, a 90° rotation), as in the regression test’s#loader. - Obtain it as a `CSSImageValue`
The page reads the computed
background-imagethroughcomputedStyleMap().get('background-image'), yielding aCanvasImageSourceof a type not covered by the old gate. - Draw with orientation suppressed
Calling
drawImage(cssImage, 0, 0)on a canvas withimage-orientation: nonereachesRespectImageOrientationInternal; because the source is neither an ImageBitmap nor an image element, the pre-patch code returnsRespectImageOrientation()and honorsnone, drawing the image unrotated. - Observe orientation-dependent output By comparing the drawn geometry to the reference (EXIF-respecting) rendering, the page infers cross-origin EXIF/orientation properties that should not be disclosed, despite the canvas being tainted against direct readback. (inferred)
Impact Assessment
CSSImageValue to drawImage. The advisory classifies this as an information leak in Canvas with no exploited-in-the-wild status stated; the precise quantity of leaked data beyond orientation is not demonstrated by the diff.Files Changed
third_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.htmlthird_party/blink/renderer/modules/canvas/canvas2d/canvas_2d_recorder_context.cc
Audit Directions
- Type-gated security rulesSearch for security-relevant decisions guarded by
IsImageElement()/IsImageBitmap()/otherCanvasImageSourcetype checks combined withWouldTaintOrigin(); the taint condition should generally stand alone. - Cross-origin rendering discrepanciesLook for code paths where a rendering attribute (orientation, color profile, resolution, intrinsic size) is applied differently depending on source type, which can turn a non-readable tainted resource into an observable oracle.
- Stale guards after refactorsAudit conditions whose comments reference an old
CHECK/crash they were meant to avoid (e.g.SVGImage::CheckLoaded()); when the underlying crash is removed, such guards may silently become security-weakening.
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.