CVE-2025-5266
Overview
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=...>orfetch(..., {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 decidingAllowvsBlock, with internal statesAllowed/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 aloador anerrorevent.
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.
<script> fire load instead of error, turning the block decision itself into an observable side channel.Attack Path
- Attacker page
embeds
<script src="https://victim.example/probe">targeting a cross-origin resource whose type ORB will block. - Response arrives
OpaqueResponseBlocker::ValidateJavaScriptdecidesBlock, but (pre-patch) callsAllowResponse()and filters the body. - Event observation
the script element fires a
loadevent (data stripped but request “succeeded”); a genuinely failed/other-typed resource fireserror. - Oracle
the attacker’s
onload/onerrorhandlers distinguish the two outcomes, inferring cross-origin state (resource existence, MIME category, redirect behavior). - 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
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifnetwerk/protocol/http/OpaqueResponseUtils.cpp |
modified |
Files Changed
netwerk/protocol/http/OpaqueResponseUtils.cpptesting/web-platform/meta/fetch/orb/tentative/known-mime-type.sub.any.js.ini
Audit Directions
- Other ORB/CORB decision sitesgrep 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 parityreview 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 usersconfirm 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.
Patch
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]
Regression Test / PoC
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]