Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInformation leak in Canvas
DescriptionInformation leak in Canvas
ComponentCanvas
Bug ClassLogic Error
Tracker517655953
Fix commit919d6580fccb (chromium/src) +63/-2
CISA KEVNot listed
CreditedGoogle
Disclosed2026-08-25

Files Changed

  • third_party/blink/renderer/modules/canvas/canvas2d/canvas_2d_recorder_context.cc
  • third_party/blink/web_tests/external/wpt/css/css-images/image-orientation/image-orientation-none-cross-origin-canvas-css-image-value.sub.html
  • 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
From 919d6580fccb08492b4822b32403bb7e78a77cab Mon Sep 17 00:00:00 2001
From: Kevin Babbitt <kbabbitt@microsoft.com>
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 <kbabbitt@microsoft.com>
Reviewed-by: Colin Blundell <blundell@chromium.org>
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>
Loading diff…

Regression Test / PoC

shipped with the fix
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>
Loading diff…

Original Bug Report

reported by vm...@google.com

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.cc
  • third_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.

  1. 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 CSSImageValue representation using the Typed OM computedStyleMap().get('background-image') API.
  2. Set Orientation to None: The attacker sets imageOrientation = 'none' on a canvas element.
  3. Draw CSSImageValue: The attacker draws the CSSImageValue onto the canvas via ctx.drawImage(cssImageValue, 0, 0). Because the source is not recognized as restricted, the clamp is bypassed and the raw unrotated decode is drawn.
  4. 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: difference under an expensive SVG filter and measuring rendering times via requestAnimationFrame), 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.

View on issue tracker