Overview

Low
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactSide-channel information leakage in Paint
DescriptionSide-channel information leakage in Paint
ComponentPaint
Bug ClassLogic Error
Tracker502239897
Fix commitf6d967dd23ef (chromium/src) +34/-4
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-02

Files Changed

  • third_party/blink/renderer/core/paint/nine_piece_image_painter.cc
  • third_party/blink/web_tests/external/wpt/css/css-images/image-orientation/image-orientation-none-cross-origin-border-image.sub.html
  • third_party/blink/web_tests/external/wpt/css/css-images/image-orientation/reference/image-orientation-none-cross-origin-border-image-ref.html
From f6d967dd23ef0529783562cbf4e7753d5b92cc5f Mon Sep 17 00:00:00 2001
From: Fredrik Söderquist <fs@opera.com>
Date: Tue, 14 Apr 2026 08:48:47 -0700
Subject: [PATCH] Fix slice scale computation in PaintPieces()

The (potentially) zoomed image size was in some cases computed with a
different image orientation than the unzoomed image size. This could
lead to incorrect rendering and potentially leak orientation metadata.

Fixed: 502239897
Change-Id: I231862c064f2cd6820c31d64a39175a68040fac7
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7759460
Auto-Submit: Fredrik Söderquist <fs@opera.com>
Reviewed-by: Stephen Chenney <schenney@chromium.org>
Commit-Queue: Stephen Chenney <schenney@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1614504}
---

diff --git a/third_party/blink/renderer/core/paint/nine_piece_image_painter.cc b/third_party/blink/renderer/core/paint/nine_piece_image_painter.cc
index 858e491..9ffb11f0 100644
--- a/third_party/blink/renderer/core/paint/nine_piece_image_painter.cc
+++ b/third_party/blink/renderer/core/paint/nine_piece_image_painter.cc
@@ -90,9 +90,8 @@
                  const NinePieceImage& nine_piece_image,
                  Image& image,
                  const gfx::SizeF& unzoomed_image_size,
+                 RespectImageOrientationEnum respect_orientation,
                  PhysicalBoxSides sides_to_include) {
-  const RespectImageOrientationEnum respect_orientation =
-      style.ImageOrientation();
   // |image_size| is in the image's native resolution and |slice_scale| defines
   // the effective size of a CSS pixel in the image.
   const gfx::SizeF image_size = image.SizeAsFloat(respect_orientation);
@@ -208,8 +207,12 @@
   // generated or SVG), then get an image using that size. This will yield an
   // image with either "native" size (raster images) or size scaled by effective
   // zoom.
+  //
+  // Resolve the image orientation based on image origin here and pass it along
+  // for consistency to avoid leaking orientation metadata if the image is
+  // cross-origin.
   const RespectImageOrientationEnum respect_orientation =
-      style.ImageOrientation();
+      style_image->ForceOrientationIfNecessary(style.ImageOrientation());
   const gfx::SizeF default_object_size(border_image_rect.size);
   gfx::SizeF image_size = style_image->ImageSize(
       style.EffectiveZoom(), default_object_size, respect_orientation);
@@ -235,7 +238,8 @@
       inspector_paint_image_event::Data, node, *style_image,
       gfx::RectF(image->Rect()), gfx::RectF(border_image_rect));
   PaintPieces(graphics_context, border_image_rect, style, nine_piece_image,
-              *image, unzoomed_image_size, sides_to_include);
+              *image, unzoomed_image_size, respect_orientation,
+              sides_to_include);
   return true;
 }
 
diff --git a/third_party/blink/web_tests/external/wpt/css/css-images/image-orientation/image-orientation-none-cross-origin-border-image.sub.html b/third_party/blink/web_tests/external/wpt/css/css-images/image-orientation/image-orientation-none-cross-origin-border-image.sub.html
new file mode 100644
index 0000000..2e7c8dc
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/css/css-images/image-orientation/image-orientation-none-cross-origin-border-image.sub.html
@@ -0,0 +1,15 @@
+<!doctype html>
+<title>CSS Images Module Level 3: image-orientation: none / cross-origin / border-image</title>
+<meta name="fuzzy" content="maxDifference=0-2; totalPixels=0-120">
+<link rel="match" href="reference/image-orientation-none-cross-origin-border-image-ref.html">
+<link rel="help" href="https://github.com/w3c/csswg-drafts/issues/5165">
+<style>
+  #target {
+    width: 100px;
+    height: 50px;
+    border-width: 20px;
+    border-image: url("http://{{hosts[][]}}:{{ports[http][1]}}/css/css-images/image-orientation/support/exif-orientation-6-ru.jpg") 20 fill repeat;
+    image-orientation: none;
+  }
+</style>
+<div id="target"></div>
diff --git a/third_party/blink/web_tests/external/wpt/css/css-images/image-orientation/reference/image-orientation-none-cross-origin-border-image-ref.html b/third_party/blink/web_tests/external/wpt/css/css-images/image-orientation/reference/image-orientation-none-cross-origin-border-image-ref.html
new file mode 100644
index 0000000..b6583178
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/css/css-images/image-orientation/reference/image-orientation-none-cross-origin-border-image-ref.html
@@ -0,0 +1,11 @@
+<!doctype html>
+<title>CSS Images Module Level 3: image-orientation: none / border-image (reference)</title>
+<style>
+  #target {
+    width: 100px;
+    height: 50px;
+    border-width: 20px;
+    border-image: url("../support/exif-orientation-6-ru-pre-rotated.jpg") 20 fill repeat;
+  }
+</style>
+<div id="target"></div>
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-border-image.sub.html b/third_party/blink/web_tests/external/wpt/css/css-images/image-orientation/image-orientation-none-cross-origin-border-image.sub.html
new file mode 100644
index 0000000..2e7c8dc
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/css/css-images/image-orientation/image-orientation-none-cross-origin-border-image.sub.html
@@ -0,0 +1,15 @@
+<!doctype html>
+<title>CSS Images Module Level 3: image-orientation: none / cross-origin / border-image</title>
+<meta name="fuzzy" content="maxDifference=0-2; totalPixels=0-120">
+<link rel="match" href="reference/image-orientation-none-cross-origin-border-image-ref.html">
+<link rel="help" href="https://github.com/w3c/csswg-drafts/issues/5165">
+<style>
+  #target {
+    width: 100px;
+    height: 50px;
+    border-width: 20px;
+    border-image: url("http://{{hosts[][]}}:{{ports[http][1]}}/css/css-images/image-orientation/support/exif-orientation-6-ru.jpg") 20 fill repeat;
+    image-orientation: none;
+  }
+</style>
+<div id="target"></div>
diff --git a/third_party/blink/web_tests/external/wpt/css/css-images/image-orientation/reference/image-orientation-none-cross-origin-border-image-ref.html b/third_party/blink/web_tests/external/wpt/css/css-images/image-orientation/reference/image-orientation-none-cross-origin-border-image-ref.html
new file mode 100644
index 0000000..b6583178
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/css/css-images/image-orientation/reference/image-orientation-none-cross-origin-border-image-ref.html
@@ -0,0 +1,11 @@
+<!doctype html>
+<title>CSS Images Module Level 3: image-orientation: none / border-image (reference)</title>
+<style>
+  #target {
+    width: 100px;
+    height: 50px;
+    border-width: 20px;
+    border-image: url("../support/exif-orientation-6-ru-pre-rotated.jpg") 20 fill repeat;
+  }
+</style>
+<div id="target"></div>
Loading diff…

Original Bug Report

reported by vm...@google.com

Cross-origin EXIF orientation leak in NinePieceImagePainter

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: NinePieceImagePainter fails to enforce the EXIF orientation privacy invariant for cross-origin images when image-orientation: none is set. This inconsistency creates a paint-timing side channel that allows attackers to infer private EXIF orientation metadata from cross-origin images.

Affected files:

  • third_party/blink/renderer/core/paint/nine_piece_image_painter.cc

Estimated timestamp from git blame: 2024-11-14

Description

Blink enforces a privacy invariant that opaque-origin (cross-origin without CORS) images must always have their EXIF orientation respected, regardless of the CSS image-orientation property. This prevents attackers from using EXIF rotation metadata as a cross-origin oracle.

There is a potential flaw in how NinePieceImagePainter (which handles border-image, -webkit-mask-box-image, etc.) handles this invariant. While calculating the CSS pixel size (unzoomed_image_size), the code correctly calls ForceOrientationIfNecessary() to apply the mandatory rotation for cross-origin images. However, when calculating the native image size (image_size) inside the PaintPieces helper, it bypasses this security check and fetches the raw dimensions based directly on the attacker-controlled CSS property (style.ImageOrientation()).

When a cross-origin image with transposing EXIF orientation (e.g., rotated 90 degrees) is rendered with image-orientation: none, this discrepancy causes unzoomed_image_size to be rotated (Height x Width) while image_size remains unrotated (Width x Height). This creates a skewed slice_scale vector which corrupts the NinePieceImageGrid calculations, significantly altering the tile repetition counts for border edges.

Potential Exploitation Steps

Note: These are suggested steps based on static analysis. We do not currently have the capability to run a live proof of concept.

  1. An attacker embeds a cross-origin image (e.g., from a user’s private photo gallery) on their site without CORS.
  2. The attacker applies the image to a large div using border-image, and sets border-image-repeat: repeat; and image-orientation: none;.
  3. The browser renders the border. Because of the missing security check in PaintPieces, the skewed slice_scale alters the number of times the image slices are tiled (e.g., squishing or stretching the tiles).
  4. The attacker toggles the CSS between image-orientation: none and image-orientation: from-image and measures the render times using requestAnimationFrame.
  5. Because the tiling count and draw complexity change significantly when transposing EXIF data is present, the rendering times will differ measurably. The attacker uses this timing delta to infer the private transposing EXIF state of the cross-origin image.

Suggested Fix

In third_party/blink/renderer/core/paint/nine_piece_image_painter.cc, the orientation preference should be evaluated once at the top of NinePieceImagePainter::Paint using style_image->ForceOrientationIfNecessary(style.ImageOrientation()). This forced orientation value should then be passed down into PaintPieces and used for all size calculations (like image.SizeAsFloat()) and drawing operations to ensure cross-origin privacy invariants are consistently maintained across the entire drawing pipeline.

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