Medium CVSS 4.3 firefox Cross Origin 🔧 Commit mapped

Overview

Medium
Severity
4.3
CVSS
No
Exploited ITW
Fixed
Fix Status
Impactmoderate
DescriptionScript elements loading cross-origin resources generated load and error events which leaked information enabling XS-Leaks attacks.
ComponentNetworking
Bug ClassCross Origin
Tracker1965628
Fix commit8065bd80ce5a (firefox) +3/-27
CWECWE-200 (Information exposure)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:N/A:N
CISA KEVNot listed
CreditedJakub Szymsza
Disclosed2025-05-27

Background

ORB (Opaque Response Blocking)
Firefox’s successor to CORB; it decides whether a cross-origin “opaque” response (e.g. one fetched by <script src=...> or fetch(..., {mode:"no-cors"})) may be delivered to the requesting context based on its MIME type and a JavaScript sniff.
`OpaqueResponseBlocker`
the networking-layer state machine (netwerk/protocol/http/OpaqueResponseUtils.cpp) that holds a response while deciding Allow vs Block, with internal states Allowed/Blocked.
`OpaqueResponseFilter`
a downstream listener that strips headers/data so a blocked response cannot leak content.
XS-Leaks
cross-site leak attacks that infer cross-origin state from observable side effects such as whether a <script> element fires a load or an error event.

Root Cause Analysis

When OpaqueResponseBlocker::ValidateJavaScript reached the OpaqueResponse::Block decision, the old code did not actually block: it called self->AllowResponse() with the comment “We’ll filter the data out later”, relying on ResolveAndProcessData to install an OpaqueResponseFilter (mNext = new OpaqueResponseFilter(mNext)) that would scrub the body while the request otherwise completed “successfully”. Because the channel still completed with a success status and OnStartRequest/OnStopRequest ran normally, a <script src=...> pointing at the blocked resource fired a load event rather than an error event. The data was stripped, but the success/failure signal itself leaked — an attacker could distinguish “ORB blocked this cross-origin resource” (load) from “network/other error” (error), and thereby infer cross-origin response properties (MIME type, existence, etc.). That observable difference is the XS-Leak.

The fix makes Block mean block: self->BlockResponse(channel, NS_ERROR_FAILURE). Consequently ResolveAndProcessData no longer needs the filter path — the if (!aAllowed) { mNext = new OpaqueResponseFilter(mNext); } block is removed, and the post-OnStartRequest assertion is corrected from mState == State::Allowed to mState == State::Blocked, with the comment “We decided to block, so nothing more to do.” The web-platform .ini updates remove the [... <script src=...>] FAIL expectations, confirming that <script>-initiated opaque loads now surface as errors. The advisory’s CWE-200/CrossOrigin label is accurate; the diff shows the concrete mechanism is a logic error (allow-then-filter instead of block) that produces the observable load/error oracle.

Key insight
ORB “blocked” a response by allowing the channel to succeed and merely filtering out its bytes; the surviving success status let a cross-origin <script> fire load instead of error, turning the block decision itself into an observable side channel.

Attack Path

  1. Attacker page embeds <script src="https://victim.example/probe"> targeting a cross-origin resource whose type ORB will block.
  2. Response arrives OpaqueResponseBlocker::ValidateJavaScript decides Block, but (pre-patch) calls AllowResponse() and filters the body.
  3. Event observation the script element fires a load event (data stripped but request “succeeded”); a genuinely failed/other-typed resource fires error.
  4. Oracle the attacker’s onload/onerror handlers distinguish the two outcomes, inferring cross-origin state (resource existence, MIME category, redirect behavior).
  5. Primitive a cross-site information-disclosure oracle (XS-Leak) usable to probe authenticated cross-origin endpoints (inferred as the practical exploitation; the diff proves the load/error signal difference).

Impact Assessment

This is a cross-origin information-disclosure (XS-Leak) side channel in the networking (parent/socket) process, exploitable by any web attacker with no special privilege and only requiring the victim to load the attacker’s page (UI:R). The attacker gains a boolean oracle per request rather than direct content; strength is limited to inference of cross-origin response metadata/state, not reading the body (the body was already filtered). CVSS 4.3 (C:L only) reflects the confidentiality-only, low-bandwidth leak.

Changed Functions

FunctionChangeNotes
if
netwerk/protocol/http/OpaqueResponseUtils.cpp
modified

Files Changed

  • netwerk/protocol/http/OpaqueResponseUtils.cpp
  • testing/web-platform/meta/fetch/orb/tentative/known-mime-type.sub.any.js.ini

Audit Directions

  • Other ORB/CORB decision sites
    grep for AllowResponse() calls whose comment or logic implies “filter later” rather than actually failing the channel; the same allow-then-strip pattern may exist for other content categories.
  • Load/error event parity
    review all networking error paths that deliver a success status with an empty/filtered body, which reintroduce the same load-vs-error oracle for cross-origin subresource loads.
  • `OpaqueResponseFilter` remaining users
    confirm whether the filter is still reachable for any legitimate path now that the primary caller was removed, to avoid dead code masking a second oracle.
diff --git a/netwerk/protocol/http/OpaqueResponseUtils.cpp b/netwerk/protocol/http/OpaqueResponseUtils.cpp
index 94969648a2c..44fb272a115 100644
--- a/netwerk/protocol/http/OpaqueResponseUtils.cpp
+++ b/netwerk/protocol/http/OpaqueResponseUtils.cpp
@@ -559,8 +559,7 @@ nsresult OpaqueResponseBlocker::ValidateJavaScript(HttpBaseChannel* aChannel,
             self->AllowResponse();
             break;
           case OpaqueResponse::Block:
-            // We'll filter the data out later
-            self->AllowResponse();
+            self->BlockResponse(channel, NS_ERROR_FAILURE);
             break;
           default:
             MOZ_ASSERT_UNREACHABLE(
@@ -620,19 +619,11 @@ void OpaqueResponseBlocker::FilterResponse() {
 
 void OpaqueResponseBlocker::ResolveAndProcessData(
     HttpBaseChannel* aChannel, bool aAllowed, Maybe<ipc::Shmem>& aSharedData) {
-  if (!aAllowed) {
-    // OpaqueResponseFilter allows us to filter the headers
-    mNext = new OpaqueResponseFilter(mNext);
-  }
-
   nsresult rv = OnStartRequest(aChannel);
 
   if (!aAllowed || NS_FAILED(rv)) {
-    MOZ_ASSERT_IF(!aAllowed, mState == State::Allowed);
-    // No need to call OnDataAvailable because
-    //   1. The input stream is consumed by
-    //     OpaqueResponseBlocker::OnDataAvailable already
-    //   2. We don't want to pass any data over
+    MOZ_ASSERT_IF(!aAllowed, mState == State::Blocked);
+    // We decided to block, so nothing more to do.
     MaybeRunOnStopRequest(aChannel);
     return;
   }
diff --git a/testing/web-platform/meta/fetch/orb/tentative/known-mime-type.sub.any.js.ini b/testing/web-platform/meta/fetch/orb/tentative/known-mime-type.sub.any.js.ini
index 1502f4e6d2a..bb9c7c8677d 100644
--- a/testing/web-platform/meta/fetch/orb/tentative/known-mime-type.sub.any.js.ini
+++ b/testing/web-platform/meta/fetch/orb/tentative/known-mime-type.sub.any.js.ini
@@ -3,33 +3,18 @@
   [ORB should block opaque font/ttf: fetch(..., {mode: "no-cors"})]
     expected: FAIL
 
-  [ORB should block opaque font/ttf: <script src=...>]
-    expected: FAIL
-
   [ORB should block opaque text/plain: fetch(..., {mode: "no-cors"})]
     expected: FAIL
 
-  [ORB should block opaque text/plain: <script src=...>]
-    expected: FAIL
-
   [ORB should block opaque application/json (non-empty): fetch(..., {mode: "no-cors"})]
     expected: FAIL
 
-  [ORB should block opaque application/json (non-empty): <script src=...>]
-    expected: FAIL
-
   [ORB should block opaque application/json (empty): fetch(..., {mode: "no-cors"})]
     expected: FAIL
 
-  [ORB should block opaque application/json (empty): <script src=...>]
-    expected: FAIL
-
   [ORB should block opaque application/json which contains non ascii characters: fetch(..., {mode: "no-cors"})]
     expected: FAIL
 
-  [ORB should block opaque application/json which contains non ascii characters: <script src=...>]
-    expected: FAIL
-
 
 [known-mime-type.sub.any.worker.html]
   prefs: [browser.opaqueResponseBlocking:true, browser.opaqueResponseBlocking.filterFetchResponse:2]
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/testing/web-platform/meta/fetch/orb/tentative/known-mime-type.sub.any.js.ini b/testing/web-platform/meta/fetch/orb/tentative/known-mime-type.sub.any.js.ini
index 1502f4e6d2a..bb9c7c8677d 100644
--- a/testing/web-platform/meta/fetch/orb/tentative/known-mime-type.sub.any.js.ini
+++ b/testing/web-platform/meta/fetch/orb/tentative/known-mime-type.sub.any.js.ini
@@ -3,33 +3,18 @@
   [ORB should block opaque font/ttf: fetch(..., {mode: "no-cors"})]
     expected: FAIL
 
-  [ORB should block opaque font/ttf: <script src=...>]
-    expected: FAIL
-
   [ORB should block opaque text/plain: fetch(..., {mode: "no-cors"})]
     expected: FAIL
 
-  [ORB should block opaque text/plain: <script src=...>]
-    expected: FAIL
-
   [ORB should block opaque application/json (non-empty): fetch(..., {mode: "no-cors"})]
     expected: FAIL
 
-  [ORB should block opaque application/json (non-empty): <script src=...>]
-    expected: FAIL
-
   [ORB should block opaque application/json (empty): fetch(..., {mode: "no-cors"})]
     expected: FAIL
 
-  [ORB should block opaque application/json (empty): <script src=...>]
-    expected: FAIL
-
   [ORB should block opaque application/json which contains non ascii characters: fetch(..., {mode: "no-cors"})]
     expected: FAIL
 
-  [ORB should block opaque application/json which contains non ascii characters: <script src=...>]
-    expected: FAIL
-
 
 [known-mime-type.sub.any.worker.html]
   prefs: [browser.opaqueResponseBlocking:true, browser.opaqueResponseBlocking.filterFetchResponse:2]
Loading diff…