CVE-2026-11289
Overview
Files Changed
third_party/blink/renderer/core/paint/nine_piece_image_painter.ccthird_party/blink/web_tests/external/wpt/css/css-images/image-orientation/image-orientation-none-cross-origin-border-image.sub.htmlthird_party/blink/web_tests/external/wpt/css/css-images/image-orientation/reference/image-orientation-none-cross-origin-border-image-ref.html
Patch
From f6d967dd23ef0529783562cbf4e7753d5b92cc5f Mon Sep 17 00:00:00 2001 From: Fredrik Söderquist <[email protected]> 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 <[email protected]> Reviewed-by: Stephen Chenney <[email protected]> Commit-Queue: Stephen Chenney <[email protected]> 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>
Regression Test / PoC
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>
Original Bug Report
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.
- An attacker embeds a cross-origin image (e.g., from a user’s private photo gallery) on their site without CORS.
- The attacker applies the image to a large
divusingborder-image, and setsborder-image-repeat: repeat;andimage-orientation: none;. - The browser renders the border. Because of the missing security check in
PaintPieces, the skewedslice_scalealters the number of times the image slices are tiled (e.g., squishing or stretching the tiles). - The attacker toggles the CSS between
image-orientation: noneandimage-orientation: from-imageand measures the render times usingrequestAnimationFrame. - 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.