Medium watch CVSS 6.3 chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
6.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInappropriate implementation in Canvas
DescriptionInappropriate implementation in Canvas
ComponentCanvas
Sub-componentBlink
Bug ClassLogic Error
Action tierwatch — information disclosure — security-relevant, below the RCE-class bar
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker497821223
Fix commitd5009e4f67e4 (chromium/src) +40/-1
CWECWE-269
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:L/A:L
CISA KEVNot listed
CreditedGoogle
Reported2026-03-30 (per vendor advisory)
Fix landed2026-04-01 (public git)
Disclosed2026-05-05
Patch gap34 days — fix public in source before the advisory

Background

`OffscreenCanvas`
Blink’s canvas object usable off the main thread, which tracks cross-origin taint via the origin_clean_ flag.
`origin_clean_`
the per-canvas flag that, when false, marks the canvas as tainted by cross-origin content and blocks pixel readback (e.g. getImageData/toDataURL/transferToImageBitmap).
`ImageBitmapRenderingContext`
a context type whose contents come from a transferred ImageBitmap (via transferFromImageBitmap), which may itself be origin-unclean.
`context_->Reset()`
resets the context’s backing store during a canvas size change.

Root Cause Analysis

On a canvas size change, the branch handling IsRenderingContext2D() || IsImageBitmapRenderingContext() called context_->Reset() and then unconditionally set origin_clean_ = true. For a 2D context a reset genuinely discards drawn content, so re-establishing a clean origin is defensible; but for an ImageBitmapRenderingContext the transferred bitmap’s taint must persist, and forcing origin_clean_ = true on resize cleared the cross-origin taint of content that was never legitimately re-created clean. As the commit notes, the spec does not say resizing should reset the image or mark the origin clean for this context type, so the invariant “a canvas tainted by cross-origin content stays tainted until clean content replaces it” was violated.

The fix guards the assignment with if (context_->IsRenderingContext2D()), so origin_clean_ is only reset to true for the 2D case and the bitmap-renderer case retains its taint. The regression test confirms that after transferring a tainted ImageBitmap and resizing, OriginClean() remains false while the context stays paintable.

Key insight
The core mistake was unconditionally setting origin_clean_ = true on resize for both context types, which laundered the cross-origin taint of an ImageBitmapRenderingContext; scoping the reset to IsRenderingContext2D() preserves the taint flag where it must persist.

Attack Path

  1. Taint a bitmap A page obtains a cross-origin / origin-unclean ImageBitmap (the test sets SetOriginClean(false) on the source image).
  2. Transfer into a bitmap-renderer canvas The page calls transferFromImageBitmap on an OffscreenCanvas using an ImageBitmapRenderingContext, making the canvas origin-unclean (OriginClean() false).
  3. Resize the canvas The page changes the canvas width/height (e.g. setWidth(101)), reaching the resize branch that, pre-patch, runs context_->Reset() and sets origin_clean_ = true.
  4. Read back cross-origin pixels With the taint cleared, the page reads the now-“clean” canvas (e.g. getImageData/toDataURL), exfiltrating cross-origin image data it should not be able to read.

Impact Assessment

A web page can bypass the canvas same-origin taint for an ImageBitmapRenderingContext by resizing after transferring a tainted bitmap, enabling cross-origin image pixel disclosure from within the renderer. The advisory rates this medium severity (inappropriate implementation in Canvas) and does not report in-the-wild exploitation. The precondition is scripting ability to transfer a cross-origin-tainted ImageBitmap into a bitmap-renderer canvas and then resize it.

Changed Functions

FunctionChangeNotes
TEST_F
third_party/blink/renderer/modules/canvas/offscreencanvas/offscreen_canvas_test.cc
modified

Files Changed

  • third_party/blink/renderer/core/offscreencanvas/offscreen_canvas.cc
  • third_party/blink/renderer/modules/canvas/offscreencanvas/offscreen_canvas_test.cc

Audit Directions

  • Taint-flag resets on lifecycle events
    Audit resize/reset/clear paths that set origin_clean_ (or equivalent taint flags) to true, confirming they only fire when content is genuinely replaced with same-origin data.
  • Context-type-agnostic branches
    Review code that treats multiple context types in one branch (IsRenderingContext2D() || IsImageBitmapRenderingContext()) for security decisions that are only valid for a subset.
  • Spec-divergent security state changes
    Cross-check places that mutate same-origin/taint state against the HTML canvas security spec, flagging state transitions the spec does not sanction.
From d5009e4f67e453db324302c2960300ffe2164802 Mon Sep 17 00:00:00 2001
From: Colin Blundell <blundell@chromium.org>
Date: Wed, 01 Apr 2026 00:57:40 -0700
Subject: [PATCH] [Blink] Don't set origin_clean_ on ImageBitmapRendererContext resize

This came in here [1], which seems unintentional: that CL looks like it
was just ensuring that a CanvasResourceProvider created by the context
to store the image contents has its size properly updated after the
context's size changes. The spec does not indicate that origin_clean_
should be set here [2], or that the image should be reset [3].

[1] https://chromium-review.googlesource.com/c/chromium/src/+/5348468
[2] https://html.spec.whatwg.org/multipage/canvas.html#security-with-canvas-elements
[3] https://html.spec.whatwg.org/multipage/canvas.html#the-canvas-element:attr-canvas-width-5

Bug: 497821223
Change-Id: Ifa8c12a6c4a28199856db43767d2885c768bf258
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7710702
Reviewed-by: Vasiliy Telezhnikov <vasilyt@chromium.org>
Commit-Queue: Colin Blundell <blundell@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1608341}
---

diff --git a/third_party/blink/renderer/core/offscreencanvas/offscreen_canvas.cc b/third_party/blink/renderer/core/offscreencanvas/offscreen_canvas.cc
index 40b82f5c..292cf48 100644
--- a/third_party/blink/renderer/core/offscreencanvas/offscreen_canvas.cc
+++ b/third_party/blink/renderer/core/offscreencanvas/offscreen_canvas.cc
@@ -176,7 +176,9 @@
     } else if (context_->IsRenderingContext2D() ||
                context_->IsImageBitmapRenderingContext()) {
       context_->Reset();
-      origin_clean_ = true;
+      if (context_->IsRenderingContext2D()) {
+        origin_clean_ = true;
+      }
     }
     dirty_rect_for_commit_ = SkIRect::MakeWH(Size().width(), Size().height());
     context_->DidDraw(CanvasPerformanceMonitor::DrawType::kOther);
diff --git a/third_party/blink/renderer/modules/canvas/offscreencanvas/offscreen_canvas_test.cc b/third_party/blink/renderer/modules/canvas/offscreencanvas/offscreen_canvas_test.cc
index 51cb4317..dd888e5 100644
--- a/third_party/blink/renderer/modules/canvas/offscreencanvas/offscreen_canvas_test.cc
+++ b/third_party/blink/renderer/modules/canvas/offscreencanvas/offscreen_canvas_test.cc
@@ -20,6 +20,7 @@
 #include "third_party/blink/renderer/core/html/canvas/unique_font_selector.h"
 #include "third_party/blink/renderer/core/imagebitmap/image_bitmap.h"
 #include "third_party/blink/renderer/modules/canvas/htmlcanvas/html_canvas_element_module.h"
+#include "third_party/blink/renderer/modules/canvas/imagebitmap/image_bitmap_rendering_context.h"
 #include "third_party/blink/renderer/modules/canvas/offscreencanvas2d/offscreen_canvas_rendering_context_2d.h"
 #include "third_party/blink/renderer/platform/graphics/canvas_resource.h"
 #include "third_party/blink/renderer/platform/graphics/gpu/shared_gpu_context.h"
@@ -28,9 +29,11 @@
 #include "third_party/blink/renderer/platform/graphics/test/mock_compositor_frame_sink.h"
 #include "third_party/blink/renderer/platform/graphics/test/mock_embedded_frame_sink_provider.h"
 #include "third_party/blink/renderer/platform/graphics/test/test_webgraphics_shared_image_interface_provider.h"
+#include "third_party/blink/renderer/platform/graphics/unaccelerated_static_bitmap_image.h"
 #include "third_party/blink/renderer/platform/testing/runtime_enabled_features_test_helpers.h"
 #include "third_party/blink/renderer/platform/testing/task_environment.h"
 #include "third_party/blink/renderer/platform/testing/testing_platform_support.h"
+#include "third_party/skia/include/core/SkSurface.h"
 
 using ::testing::_;
 using ::testing::Combine;
@@ -304,6 +307,40 @@
   EXPECT_TRUE(SharedGpuContext::IsValidWithoutRestoringForTesting());
 }
 
+TEST_F(OffscreenCanvasTest, BitmapRendererResizePreservesTaint) {
+  auto* canvas = MakeGarbageCollected<OffscreenCanvas>(
+      GetDocument().GetExecutionContext(), gfx::Size(100, 100));
+  CanvasContextCreationAttributesCore attrs;
+  auto* context = static_cast<ImageBitmapRenderingContext*>(
+      canvas->GetCanvasRenderingContext(
+          GetDocument().GetExecutionContext(),
+          CanvasRenderingContext::CanvasRenderingAPI::kBitmaprenderer, attrs));
+  ASSERT_NE(context, nullptr);
+
+  // Create a tainted ImageBitmap.
+  SkImageInfo info = SkImageInfo::MakeN32Premul(10, 10);
+  sk_sp<SkSurface> surface = SkSurfaces::Raster(info);
+  auto image =
+      UnacceleratedStaticBitmapImage::Create(surface->makeImageSnapshot());
+  image->SetOriginClean(false);
+  auto* bitmap = MakeGarbageCollected<ImageBitmap>(image);
+  EXPECT_FALSE(bitmap->OriginClean());
+
+  // Transfer tainted bitmap to canvas.
+  context->transferFromImageBitmap(bitmap, ASSERT_NO_EXCEPTION);
+  EXPECT_FALSE(canvas->OriginClean());
+  EXPECT_TRUE(context->IsPaintable());
+
+  // Resize the canvas.
+  canvas->setWidth(101);
+
+  // origin_clean_ should still be false.
+  EXPECT_FALSE(canvas->OriginClean());
+  EXPECT_TRUE(context->IsPaintable());
+
+  CanvasRenderingContext::GetCanvasPerformanceMonitor().ResetForTesting();
+}
+
 const TestParams kTestCases[] = {
     {false /* alpha */, false /* desynchronized */},
     {false, true},
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/third_party/blink/renderer/modules/canvas/offscreencanvas/offscreen_canvas_test.cc b/third_party/blink/renderer/modules/canvas/offscreencanvas/offscreen_canvas_test.cc
index 51cb4317..dd888e5 100644
--- a/third_party/blink/renderer/modules/canvas/offscreencanvas/offscreen_canvas_test.cc
+++ b/third_party/blink/renderer/modules/canvas/offscreencanvas/offscreen_canvas_test.cc
@@ -20,6 +20,7 @@
 #include "third_party/blink/renderer/core/html/canvas/unique_font_selector.h"
 #include "third_party/blink/renderer/core/imagebitmap/image_bitmap.h"
 #include "third_party/blink/renderer/modules/canvas/htmlcanvas/html_canvas_element_module.h"
+#include "third_party/blink/renderer/modules/canvas/imagebitmap/image_bitmap_rendering_context.h"
 #include "third_party/blink/renderer/modules/canvas/offscreencanvas2d/offscreen_canvas_rendering_context_2d.h"
 #include "third_party/blink/renderer/platform/graphics/canvas_resource.h"
 #include "third_party/blink/renderer/platform/graphics/gpu/shared_gpu_context.h"
@@ -28,9 +29,11 @@
 #include "third_party/blink/renderer/platform/graphics/test/mock_compositor_frame_sink.h"
 #include "third_party/blink/renderer/platform/graphics/test/mock_embedded_frame_sink_provider.h"
 #include "third_party/blink/renderer/platform/graphics/test/test_webgraphics_shared_image_interface_provider.h"
+#include "third_party/blink/renderer/platform/graphics/unaccelerated_static_bitmap_image.h"
 #include "third_party/blink/renderer/platform/testing/runtime_enabled_features_test_helpers.h"
 #include "third_party/blink/renderer/platform/testing/task_environment.h"
 #include "third_party/blink/renderer/platform/testing/testing_platform_support.h"
+#include "third_party/skia/include/core/SkSurface.h"
 
 using ::testing::_;
 using ::testing::Combine;
@@ -304,6 +307,40 @@
   EXPECT_TRUE(SharedGpuContext::IsValidWithoutRestoringForTesting());
 }
 
+TEST_F(OffscreenCanvasTest, BitmapRendererResizePreservesTaint) {
+  auto* canvas = MakeGarbageCollected<OffscreenCanvas>(
+      GetDocument().GetExecutionContext(), gfx::Size(100, 100));
+  CanvasContextCreationAttributesCore attrs;
+  auto* context = static_cast<ImageBitmapRenderingContext*>(
+      canvas->GetCanvasRenderingContext(
+          GetDocument().GetExecutionContext(),
+          CanvasRenderingContext::CanvasRenderingAPI::kBitmaprenderer, attrs));
+  ASSERT_NE(context, nullptr);
+
+  // Create a tainted ImageBitmap.
+  SkImageInfo info = SkImageInfo::MakeN32Premul(10, 10);
+  sk_sp<SkSurface> surface = SkSurfaces::Raster(info);
+  auto image =
+      UnacceleratedStaticBitmapImage::Create(surface->makeImageSnapshot());
+  image->SetOriginClean(false);
+  auto* bitmap = MakeGarbageCollected<ImageBitmap>(image);
+  EXPECT_FALSE(bitmap->OriginClean());
+
+  // Transfer tainted bitmap to canvas.
+  context->transferFromImageBitmap(bitmap, ASSERT_NO_EXCEPTION);
+  EXPECT_FALSE(canvas->OriginClean());
+  EXPECT_TRUE(context->IsPaintable());
+
+  // Resize the canvas.
+  canvas->setWidth(101);
+
+  // origin_clean_ should still be false.
+  EXPECT_FALSE(canvas->OriginClean());
+  EXPECT_TRUE(context->IsPaintable());
+
+  CanvasRenderingContext::GetCanvasPerformanceMonitor().ResetForTesting();
+}
+
 const TestParams kTestCases[] = {
     {false /* alpha */, false /* desynchronized */},
     {false, true},
Loading diff…

Original Bug Report

reported by vm...@google.com

SOP bypass in OffscreenCanvas resize with bitmaprenderer context

Project Fortify, an experimental security project, has identified the following potential security issue.

Overview: Resizing an OffscreenCanvas with a bitmaprenderer context incorrectly resets its origin_clean_ flag to true without actually clearing the underlying image data. This state desynchronization potentially allows an attacker to bypass the Same-Origin Policy and read cross-origin image data. An attacker could exploit this by transferring a tainted ImageBitmap to the canvas, resizing it, and then extracting the protected pixels.

Affected files:

  • third_party/blink/renderer/core/offscreencanvas/offscreen_canvas.cc
  • third_party/blink/renderer/modules/canvas/imagebitmap/image_bitmap_rendering_context.cc

Estimated timestamp from git blame: 2025-07-08

Description

A potential Same-Origin Policy (SOP) bypass exists in Blink’s OffscreenCanvas implementation when using the bitmaprenderer (ImageBitmap) rendering context. When an OffscreenCanvas is resized, its internal origin_clean_ flag is unconditionally reset to true if it uses a bitmaprenderer context. However, the actual image data stored in the rendering context is not cleared during this resize operation. This allows scripts to read back cross-origin pixel data that should be protected by the taint flag.

Note: The following steps and analysis are based on static code analysis; our tooling currently does not have the capability to run a live proof-of-concept.

Root Cause Analysis

The issue resides in OffscreenCanvas::SetSize within third_party/blink/renderer/core/offscreencanvas/offscreen_canvas.cc. During a resize, the following logic executes:

    } else if (context_->IsRenderingContext2D() ||
               context_->IsImageBitmapRenderingContext()) {
      context_->Reset();
      origin_clean_ = true;
    }

While context_->Reset() is called, the implementation for ImageBitmapRenderingContext (third_party/blink/renderer/modules/canvas/imagebitmap/image_bitmap_rendering_context.cc) fails to clear the image stored in its ImageLayerBridge:

void ImageBitmapRenderingContext::Reset() {
  CHECK(Host());
  CHECK(Host()->IsOffscreenCanvas());
  resource_provider_for_offscreen_canvas_.reset();
  Host()->DiscardResources();
}

Because the image_layer_bridge_ is not cleared or disposed of, the OffscreenCanvas incorrectly reports its state as clean (OriginClean() == true) while the context still retains the tainted, cross-origin image pixels in memory.

Potential Attack Steps

An attacker could potentially trigger this vulnerability by following these steps:

  1. Load a cross-origin image (without CORS) from a victim site using an HTMLImageElement (e.g., an authenticated avatar or private data).
  2. Convert the cross-origin image into an ImageBitmap using createImageBitmap(img). This bitmap is correctly marked as not origin-clean (tainted).
  3. Create an OffscreenCanvas and obtain a bitmaprenderer context: const oc = new OffscreenCanvas(100, 100); const ctx = oc.getContext('bitmaprenderer');
  4. Transfer the tainted bitmap to the canvas via ctx.transferFromImageBitmap(taintedBitmap). The canvas’s origin_clean_ flag is correctly set to false.
  5. Resize the canvas to trigger the bug: oc.width = 101;. This invokes SetSize, which incorrectly resets origin_clean_ to true but leaves the tainted pixels intact in the context.
  6. Exfiltrate the pixel data by calling await oc.convertToBlob(). Since OriginClean() now incorrectly returns true, the security check passes, and the Blob is populated with the protected cross-origin pixels.

Suggested Fix

Modify ImageBitmapRenderingContext::Reset() to ensure the underlying image data is actually cleared. This can be done by replacing the image with a transparent black bitmap or dropping the reference in the layer bridge entirely. For example:

void ImageBitmapRenderingContext::Reset() {
  CHECK(Host());
  CHECK(Host()->IsOffscreenCanvas());
  resource_provider_for_offscreen_canvas_.reset();
  ResetInternalBitmapToBlackTransparent(Host()->width(), Host()->height()); // Clear the image
  Host()->DiscardResources();
}

Alternatively, call image_layer_bridge_->Dispose() or image_layer_bridge_->SetImage(nullptr) if appropriate for the bridge’s lifecycle.

Evaluated with Chrome root at commit: a9cbf6e8b275fe4147435aa905f3b7f5a656f5f0


Results from 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