Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInappropriate implementation in Canvas
DescriptionInappropriate implementation in Canvas
ComponentCanvas
Bug ClassLogic Error
Tracker497821223
Fix commitd5009e4f67e4 (chromium/src) +40/-1
CISA KEVNot listed
CreditedGoogle
Disclosed2026-05-05

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