High firefox Info Disclosure 🔧 Commit mapped

Overview

High
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Impacthigh
DescriptionInformation disclosure in the Graphics: ImageLib component
ComponentImageLib
Bug ClassInfo Disclosure
Tracker2050626
Fix commit27a3a8689a00 (firefox) +36/-15
CISA KEVNot listed
Creditedsatyamasd
Disclosed2026-07-21

Background

`imgRequest`
The ImageLib object that owns the network load of a single image, holding the nsITimedChannel and shared image data behind one or more proxies.
`imgRequestProxy`
A per-consumer handle onto an imgRequest that exposes image state (including redirect provenance) to callers such as the canvas/CORS machinery.
Cross-origin redirect tainting
The rule that an image whose load chain crossed an origin boundary must taint a <canvas> so its pixels cannot be read back by script.
`nsITimedChannel`
The channel-side object that records timing and redirect metadata, including GetAllRedirectsSameOriginIgnoringInternal, and is only alive during the load.

Root Cause Analysis

imgRequestProxy::GetHadCrossOriginRedirects computed its answer by calling TimedChannel() and querying GetAllRedirectsSameOriginIgnoringInternal on it, but the timed channel is set to nullptr in imgRequest::OnStopRequest as soon as the load finishes. Once the channel was gone, the getter fell through to its *aHadCrossOriginRedirects = false default, silently reporting “no cross-origin redirect” for any image whose load had already completed — the common case by the time a <canvas> reads pixels. The security invariant that a load chain containing a cross-origin redirect must always taint the canvas was therefore violated after OnStopRequest.

The fix latches the redirect result into a persistent mHadCrossOriginRedirects member inside OnStopRequest (before dropping mTimedChannel) and adds imgRequest::HadCrossOriginRedirects(), which reads the live channel while it exists and otherwise returns the latched value. The proxy now delegates to GetOwner()->HadCrossOriginRedirects(), so the answer survives the lifetime of the channel.

Key insight
The single mistake was deriving a security-critical, permanent property (did the load chain cross origins) from an ephemeral object (nsITimedChannel) that is destroyed at load completion, so the answer defaulted to the insecure false afterward. The fix persists the value in imgRequest at OnStopRequest time and reads it from there, decoupling the taint decision from the channel’s lifetime.

Attack Path

  1. Host a redirecting image The attacker serves an image URL that redirects across an origin boundary (including a same-origin to cross-origin to same-origin bounce-back) to bring cross-origin pixel data into a same-origin-looking response.
  2. Load into the victim page A victim page draws that image; while loading, mTimedChannel correctly reflects the cross-origin redirect.
  3. Wait for load completion After imgRequest::OnStopRequest nulls mTimedChannel, the redirect provenance is no longer retrievable from the channel.
  4. Draw and read back Script draws the now-loaded image to a <canvas>; GetHadCrossOriginRedirects returns false, so the canvas is not tainted.
  5. Exfiltrate pixels The script calls getImageData/toDataURL and reads cross-origin pixel data that should have been blocked.

Impact Assessment

An attacker-controlled web page in the content process can read back pixel data from cross-origin images that were reached via a redirect, defeating the canvas same-origin protection and disclosing information the origin should not expose. The preconditions are that the image load complete (so mTimedChannel is dropped) before the canvas read, and that the load chain contain a real cross-origin redirect that is not merely an internal/service-worker substitution. Impact is information disclosure of cross-origin image contents; no memory corruption is implied by the diff.

Changed Functions

FunctionChangeNotes
if
image/imgRequest.cpp
modified
GetHadCrossOriginRedirects
image/imgRequestProxy.cpp
modified
if
image/imgRequestProxy.cpp
modified

Files Changed

  • image/imgRequest.cpp
  • image/imgRequest.h
  • image/imgRequestProxy.cpp
  • testing/web-platform/meta/fetch/images/canvas-remote-read-remote-image-redirect.html.ini

Audit Directions

  • Ephemeral-object-derived security state
    Audit any security or CORS decision computed from nsITimedChannel, the underlying nsIChannel, or other load-time objects that are released at OnStopRequest; such values must be latched into the owning object.
  • Default-to-insecure fallbacks
    Flag getters whose “object missing” path defaults a taint/permission flag to false/allow, since a lifetime change silently converts them into a bypass.
  • Redirect provenance across proxies
    Verify that imgRequestProxy consumers read redirect and principal provenance from the owning imgRequest, and that internal versus real cross-origin redirects are distinguished consistently wherever tainting is decided.
diff --git a/image/imgRequest.cpp b/image/imgRequest.cpp
index a7f7a8d8036..5b3676a9afa 100644
--- a/image/imgRequest.cpp
+++ b/image/imgRequest.cpp
@@ -622,6 +622,22 @@ bool imgRequest::HadInsecureRedirect() const {
   return mHadInsecureRedirect;
 }
 
+bool imgRequest::HadCrossOriginRedirects() const {
+  // While the channel is still around (during the load) read the authoritative
+  // value from it; afterwards fall back to the value latched in OnStopRequest.
+  // Ignore internal redirects (e.g. a service worker substituting a same-origin
+  // response for a cross-origin request): those are not cross-origin data flow
+  // and must not taint. Real cross-origin redirects (incl. bounce-backs) count.
+  if (mTimedChannel) {
+    bool allRedirectsSameOrigin = false;
+    return NS_SUCCEEDED(
+               mTimedChannel->GetAllRedirectsSameOriginIgnoringInternal(
+                   &allRedirectsSameOrigin)) &&
+           !allRedirectsSameOrigin;
+  }
+  return mHadCrossOriginRedirects;
+}
+
 /** nsIRequestObserver methods **/
 
 NS_IMETHODIMP
@@ -817,6 +833,18 @@ imgRequest::OnStopRequest(nsIRequest* aRequest, nsresult status) {
     progressTracker->SyncNotifyProgress(progress);
   }
 
+  // Store whether the load involved a cross-origin redirect before we drop the
+  // timed channel. Internal redirects (e.g. a service worker serving a
+  // same-origin response for a cross-origin request) are ignored so we don't
+  // over-taint; real cross-origin redirects (incl. bounce-backs) still count.
+  if (mTimedChannel) {
+    bool allRedirectsSameOrigin = false;
+    mHadCrossOriginRedirects =
+        NS_SUCCEEDED(mTimedChannel->GetAllRedirectsSameOriginIgnoringInternal(
+            &allRedirectsSameOrigin)) &&
+        !allRedirectsSameOrigin;
+  }
+
   mTimedChannel = nullptr;
   return NS_OK;
 }
diff --git a/image/imgRequest.h b/image/imgRequest.h
index 33497c94782..febd6d65529 100644
--- a/image/imgRequest.h
+++ b/image/imgRequest.h
@@ -165,6 +165,10 @@ class imgRequest final : public nsIThreadRetargetableStreamListener,
 
   nsITimedChannel* GetTimedChannel() const { return mTimedChannel; }
 
+  /// Returns true if any redirect in the chain that loaded this image was
+  /// cross-origin.
+  bool HadCrossOriginRedirects() const;
+
   imgCacheValidator* GetValidator() const { return mValidator; }
   void SetValidator(imgCacheValidator* aValidator) { mValidator = aValidator; }
 
@@ -285,6 +289,8 @@ class imgRequest final : public nsIThreadRetargetableStreamListener,
   bool mIsCrossSiteNoCORSRequest;
 
   bool mShouldReportRenderTimeForLCP;
+  // True if any redirect in the chain that loaded this image was cross-origin.
+  bool mHadCrossOriginRedirects = false;
   // SVGs can't be OffMainThread for example
   bool mOffMainThreadData = false;
 
diff --git a/image/imgRequestProxy.cpp b/image/imgRequestProxy.cpp
index 1067f9ba827..9a41d868e02 100644
--- a/image/imgRequestProxy.cpp
+++ b/image/imgRequestProxy.cpp
@@ -873,17 +873,8 @@ imgRequestProxy::GetImagePrincipal(nsIPrincipal** aPrincipal) {
 
 NS_IMETHODIMP
 imgRequestProxy::GetHadCrossOriginRedirects(bool* aHadCrossOriginRedirects) {
-  *aHadCrossOriginRedirects = false;
-
-  nsCOMPtr<nsITimedChannel> timedChannel = TimedChannel();
-  if (timedChannel) {
-    bool allRedirectsSameOrigin = false;
-    *aHadCrossOriginRedirects =
-        NS_SUCCEEDED(timedChannel->GetAllRedirectsSameOriginIgnoringInternal(
-            &allRedirectsSameOrigin)) &&
-        !allRedirectsSameOrigin;
-  }
-
+  *aHadCrossOriginRedirects =
+      GetOwner() ? GetOwner()->HadCrossOriginRedirects() : false;
   return NS_OK;
 }
 
diff --git a/testing/web-platform/meta/fetch/images/canvas-remote-read-remote-image-redirect.html.ini b/testing/web-platform/meta/fetch/images/canvas-remote-read-remote-image-redirect.html.ini
deleted file mode 100644
index 33630ce7eba..00000000000
--- a/testing/web-platform/meta/fetch/images/canvas-remote-read-remote-image-redirect.html.ini
+++ /dev/null
@@ -1,4 +0,0 @@
-[canvas-remote-read-remote-image-redirect.html]
-  [Load a no-cors image from a same-origin URL that redirects to a cross-origin URL that redirects to the initial origin]
-    expected: FAIL
-
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/testing/web-platform/meta/fetch/images/canvas-remote-read-remote-image-redirect.html.ini b/testing/web-platform/meta/fetch/images/canvas-remote-read-remote-image-redirect.html.ini
deleted file mode 100644
index 33630ce7eba..00000000000
--- a/testing/web-platform/meta/fetch/images/canvas-remote-read-remote-image-redirect.html.ini
+++ /dev/null
@@ -1,4 +0,0 @@
-[canvas-remote-read-remote-image-redirect.html]
-  [Load a no-cors image from a same-origin URL that redirects to a cross-origin URL that redirects to the initial origin]
-    expected: FAIL
-
Loading diff…