Overview

Low
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactPolicy bypass in CSS
DescriptionPolicy bypass in CSS
ComponentCSS
Bug ClassLogic Error
Tracker502231588
Fix commit1d5d289c59ee (chromium/src) +48/-5
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-02

Changed Functions

FunctionChangeNotes
if
third_party/blink/renderer/core/style/style_crossfade_image.cc
modified
for
third_party/blink/renderer/core/style/style_crossfade_image.cc
modified
if
third_party/blink/renderer/platform/graphics/crossfade_generated_image.cc
modified

Files Changed

  • third_party/blink/renderer/core/style/style_crossfade_image.cc
  • third_party/blink/renderer/core/style/style_crossfade_image.h
  • third_party/blink/renderer/platform/graphics/crossfade_generated_image.cc
  • third_party/blink/web_tests/external/wpt/css/css-images/cross-fade-cross-origin-orientation-ref.html
  • third_party/blink/web_tests/external/wpt/css/css-images/cross-fade-cross-origin-orientation.sub.html
From 1d5d289c59eec4fed57241f5b6cff6f5dd0162e6 Mon Sep 17 00:00:00 2001
From: Rune Lillesveen <futhark@chromium.org>
Date: Tue, 14 Apr 2026 08:51:20 -0700
Subject: [PATCH] Don't allow image-orientation to leak EXIF information via cross-fade()

Bug: 502231588
Change-Id: I2149c06cb1fc732e4cf45a2c492fe968edce9aa4
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7761585
Reviewed-by: Stephen Chenney <schenney@chromium.org>
Commit-Queue: Rune Lillesveen <futhark@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1614508}
---

diff --git a/third_party/blink/renderer/core/style/style_crossfade_image.cc b/third_party/blink/renderer/core/style/style_crossfade_image.cc
index 4634fabd..163aa84 100644
--- a/third_party/blink/renderer/core/style/style_crossfade_image.cc
+++ b/third_party/blink/renderer/core/style/style_crossfade_image.cc
@@ -287,6 +287,20 @@
   });
 }
 
+RespectImageOrientationEnum StyleCrossfadeImage::ForceOrientationIfNecessary(
+    RespectImageOrientationEnum default_orientation) const {
+  if (default_orientation == kRespectImageOrientation) {
+    return kRespectImageOrientation;
+  }
+  for (const auto& image : images_) {
+    if (image && image->ForceOrientationIfNecessary(default_orientation) ==
+                     kRespectImageOrientation) {
+      return kRespectImageOrientation;
+    }
+  }
+  return default_orientation;
+}
+
 // Calculates the actual value of the percentage for each image,
 // and converts to 0..1 weights. See
 // https://drafts.csswg.org/css-images-4/#cross-fade-function:
diff --git a/third_party/blink/renderer/core/style/style_crossfade_image.h b/third_party/blink/renderer/core/style/style_crossfade_image.h
index adc9035..b2c09df 100644
--- a/third_party/blink/renderer/core/style/style_crossfade_image.h
+++ b/third_party/blink/renderer/core/style/style_crossfade_image.h
@@ -58,6 +58,8 @@
 
   WrappedImagePtr Data() const override;
   bool KnownToBeOpaque(const Document&, const ComputedStyle&) const override;
+  RespectImageOrientationEnum ForceOrientationIfNecessary(
+      RespectImageOrientationEnum) const override;
 
   void Trace(Visitor*) const override;
 
diff --git a/third_party/blink/renderer/platform/graphics/crossfade_generated_image.cc b/third_party/blink/renderer/platform/graphics/crossfade_generated_image.cc
index 3ea9818c..8f6d69d6 100644
--- a/third_party/blink/renderer/platform/graphics/crossfade_generated_image.cc
+++ b/third_party/blink/renderer/platform/graphics/crossfade_generated_image.cc
@@ -55,11 +55,6 @@
   for (unsigned image_idx = 0; image_idx < images_.size(); ++image_idx) {
     ImageDrawOptions image_draw_options(draw_options);
     if (image_idx == 0) {
-      // TODO(junov): This code should probably be propagating the
-      // RespectImageOrientationEnum from CrossfadeGeneratedImage::draw(). Code
-      // was written this way during refactoring to avoid modifying existing
-      // behavior, but this warrants further investigation. crbug.com/472634
-      image_draw_options.respect_orientation = kDoNotRespectImageOrientation;
       image_flags.setBlendMode(SkBlendMode::kSrcOver);
     } else {
       image_flags.setBlendMode(SkBlendMode::kPlus);
diff --git a/third_party/blink/web_tests/external/wpt/css/css-images/cross-fade-cross-origin-orientation-ref.html b/third_party/blink/web_tests/external/wpt/css/css-images/cross-fade-cross-origin-orientation-ref.html
new file mode 100644
index 0000000..83f253d
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/css/css-images/cross-fade-cross-origin-orientation-ref.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>cross-fade() respects orientation for cross-origin images even with image-orientation: none (ref)</title>
+<style>
+  .ref {
+    width: 50px;
+    height: 100px;
+    /* This is same-origin so it respects orientation by default (from-image) */
+    background-image: url("image-orientation/support/exif-orientation-6-ru.jpg");
+    background-size: 100% 100%;
+    background-repeat: no-repeat;
+  }
+</style>
+<p>The image below should be rotated (50x100) because it is cross-origin, despite image-orientation: none.</p>
+<div class="ref"></div>
diff --git a/third_party/blink/web_tests/external/wpt/css/css-images/cross-fade-cross-origin-orientation.sub.html b/third_party/blink/web_tests/external/wpt/css/css-images/cross-fade-cross-origin-orientation.sub.html
new file mode 100644
index 0000000..23ca3c64
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/css/css-images/cross-fade-cross-origin-orientation.sub.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>cross-fade() respects orientation for cross-origin images even with image-orientation: none</title>
+<link rel="help" href="https://drafts.csswg.org/css-images-4/#cross-fade-function">
+<link rel="match" href="cross-fade-cross-origin-orientation-ref.html">
+<style>
+  .test {
+    width: 50px;
+    height: 100px;
+    image-orientation: none; /* Should be ignored for cross-origin images */
+    background-image: cross-fade(100% url("http://{{host}}:{{ports[http][1]}}/css/css-images/image-orientation/support/exif-orientation-6-ru.jpg"), 0% #fff);
+    background-size: 100% 100%;
+    background-repeat: no-repeat;
+  }
+</style>
+<p>The image below should be rotated (50x100) because it is cross-origin, despite image-orientation: none.</p>
+<div class="test"></div>
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/third_party/blink/web_tests/external/wpt/css/css-images/cross-fade-cross-origin-orientation-ref.html b/third_party/blink/web_tests/external/wpt/css/css-images/cross-fade-cross-origin-orientation-ref.html
new file mode 100644
index 0000000..83f253d
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/css/css-images/cross-fade-cross-origin-orientation-ref.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>cross-fade() respects orientation for cross-origin images even with image-orientation: none (ref)</title>
+<style>
+  .ref {
+    width: 50px;
+    height: 100px;
+    /* This is same-origin so it respects orientation by default (from-image) */
+    background-image: url("image-orientation/support/exif-orientation-6-ru.jpg");
+    background-size: 100% 100%;
+    background-repeat: no-repeat;
+  }
+</style>
+<p>The image below should be rotated (50x100) because it is cross-origin, despite image-orientation: none.</p>
+<div class="ref"></div>
diff --git a/third_party/blink/web_tests/external/wpt/css/css-images/cross-fade-cross-origin-orientation.sub.html b/third_party/blink/web_tests/external/wpt/css/css-images/cross-fade-cross-origin-orientation.sub.html
new file mode 100644
index 0000000..23ca3c64
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/css/css-images/cross-fade-cross-origin-orientation.sub.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>cross-fade() respects orientation for cross-origin images even with image-orientation: none</title>
+<link rel="help" href="https://drafts.csswg.org/css-images-4/#cross-fade-function">
+<link rel="match" href="cross-fade-cross-origin-orientation-ref.html">
+<style>
+  .test {
+    width: 50px;
+    height: 100px;
+    image-orientation: none; /* Should be ignored for cross-origin images */
+    background-image: cross-fade(100% url("http://{{host}}:{{ports[http][1]}}/css/css-images/image-orientation/support/exif-orientation-6-ru.jpg"), 0% #fff);
+    background-size: 100% 100%;
+    background-repeat: no-repeat;
+  }
+</style>
+<p>The image below should be rotated (50x100) because it is cross-origin, despite image-orientation: none.</p>
+<div class="test"></div>
Loading diff…

Original Bug Report

reported by vm...@google.com

Bypass of cross-origin EXIF-orientation clamp via CSS cross-fade()

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 without the Chrome Security team.

Overview: Blink fails to properly enforce EXIF-orientation privacy clamps for cross-origin images when they are used within a CSS cross-fade() function. StyleCrossfadeImage does not override ForceOrientationIfNecessary, and an underlying graphics layer hardcodes the first cross-faded image to draw unrotated, allowing an attacker to deduce the image’s orientation metadata via rendering side-channels.

Affected files:

  • third_party/blink/renderer/core/style/style_crossfade_image.h
  • third_party/blink/renderer/platform/graphics/crossfade_generated_image.cc
  • third_party/blink/renderer/core/style/style_crossfade_image.cc
  • third_party/blink/renderer/core/style/style_image.h
  • third_party/blink/renderer/core/paint/box_painter_base.cc
  • third_party/blink/renderer/core/style/style_fetched_image.cc
  • third_party/blink/renderer/core/animation/css_image_interpolation_type.cc

Estimated timestamp from git blame: 2023-12-13

Summary

A potential vulnerability in Blink’s handling of the CSS cross-fade() function allows a cross-origin image to bypass the EXIF-orientation privacy clamp. This clamp is designed to prevent the leakage of private metadata (specifically, the 3-bit EXIF orientation tag) from images loaded without CORS. By using cross-fade(), an attacker can force an opaque cross-origin image to render without its orientation transform applied. Because the layout box is sized according to the oriented dimensions, the unrotated image is drawn stretched or squashed. This distortion can be programmatically detected via rendering side-channels, leaking the orientation metadata.

Technical Details

To prevent cross-origin metadata leakage, Blink enforces that opaque cross-origin images must always be drawn respecting their EXIF orientation, regardless of user-provided CSS like image-orientation: none. This is implemented in StyleFetchedImage::ForceOrientationIfNecessary() (third_party/blink/renderer/core/style/style_fetched_image.cc:258), which overrides the virtual method from StyleImage.

During the paint preparation phase, BoxPainterBase::FillLayerInfo calls image->ForceOrientationIfNecessary() to apply this security clamp. However, when the background image is a cross-fade(), the image pointer refers to a StyleCrossfadeImage.

StyleCrossfadeImage (third_party/blink/renderer/core/style/style_crossfade_image.h) fails to override ForceOrientationIfNecessary(). Consequently, it inherits the default implementation from StyleImage, which simply returns the input value without performing the critical cross-origin access check. The security clamp is completely bypassed for the constituent images of the cross-fade.

Furthermore, when the actual rendering occurs, CrossfadeGeneratedImage::DrawCrossfade() (third_party/blink/renderer/platform/graphics/crossfade_generated_image.cc:38) iterates over the images to be blended. For the very first image (index 0), the code explicitly hardcodes image_draw_options.respect_orientation = kDoNotRespectImageOrientation; (noted as a TODO from crbug.com/472634).

Because StyleCrossfadeImage::ImageSize() explicitly calculates the layout dimensions using kRespectImageOrientation, the destination paint rectangle is sized for the rotated image. When DrawCrossfade() subsequently draws the first image unrotated, the raw bitmap pixels are stretched or squashed to fit the rotated bounding box.

Potential Attack Steps

Note: These are potential steps based on code analysis; a full PoC has not been executed.

  1. Host a non-square JPEG with an EXIF orientation tag (e.g., 90° CW) on https://victim.example/p.jpg without CORS headers.
  2. On an attacker origin, apply the following CSS to an element:
    .bypass {
      background-image: cross-fade(url(https://victim.example/p.jpg), transparent, 0%);
    }
    
  3. Because the cross-origin image is the first argument, DrawCrossfade() will draw it unrotated into a bounding box sized for the rotated dimensions, resulting in a distorted (stretched/squashed) rendering.
  4. The attacker can then use programmatic extraction via standard pixel-based rendering side channels (such as SVG filter timing or mix-blend-mode compositing oracles) to detect the distortion pattern. This reveals the original EXIF orientation of the opaque response.

Suggested Fix

  1. Override ForceOrientationIfNecessary: StyleCrossfadeImage must override ForceOrientationIfNecessary. The implementation should iterate through its constituent images (similar to StyleCrossfadeImage::KnownToBeOpaque). If any constituent image requires forcing orientation (i.e., returns kRespectImageOrientation when called with kDoNotRespectImageOrientation), the StyleCrossfadeImage must return kRespectImageOrientation.
  2. Fix DrawCrossfade: Remove the hardcoded kDoNotRespectImageOrientation for the first image in CrossfadeGeneratedImage::DrawCrossfade(). The method should respect the draw_options.respect_orientation passed into it by the caller for all images in the cross-fade.

Evaluated with Chrome root at commit: 096fc8fdbfacf2546485756d03f160a3d04fcc9b


Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; 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
Links in the report