Range-authorization bypass in [@ RemoteLazyInputStreamParent::RecvStreamNeeded] - content process granted a Blob slice can read the entire parent-held backing stream (cross-origin information disclosure)
Categories
(Core :: DOM: File, defect, P3)
Tracking
()
People
(Reporter: tjr, Unassigned)
References
(Blocks 1 open bug)
Details
(4 keywords)
Attachments
(6 files)
|
4.55 KB,
text/html
|
Details | |
|
7.73 KB,
patch
|
Details | Diff | Splinter Review | |
|
273 bytes,
text/plain
|
Details | |
|
1.67 KB,
text/plain
|
Details | |
|
2.58 KB,
patch
|
Details | Diff | Splinter Review | |
|
12.95 KB,
text/plain
|
Details |
PRemoteLazyInputStream is the lazy-stream IPC protocol backing Blob/File data held in the parent process. The parent keeps the real stream in a global UUID-keyed map (RemoteLazyInputStreamStorage); content processes hold actors plus a client-side view {id, start, length}. Range confinement of that data is enforced only in the untrusted content process: (1) RemoteLazyInputStreamParent::RecvStreamNeeded(aStart, aLength) performs no authorization beyond actor possession - it resolves its UUID in the global storage and serves ANY child-requested range of the FULL backing stream, because the granted {start,length} window exists only as client-side integers in the child's RemoteLazyInputStream wrapper; and (2) even when the parent-held storage entry has been deliberately narrowed to a SlicedInputStream (which the parent's structured-clone relay code creates via TakeInternalStream + re-registration when a victim shares blob.slice(...) cross-process - clear evidence the slice is an intended confinement boundary), the StreamNeeded reply serialization (SerializeIPCStream with aAllowLazy=false -> SlicedInputStream::Serialize -> nsFileInputStream::Serialize) ships SlicedInputStreamParams containing the FULL inner stream: for file-backed blobs, a dup'd OS file descriptor for the ENTIRE backing file, with start/length carried as advisory integers that only the recipient is trusted to honour.
Demonstrated end-to-end under Fission: a victim page (site A) round-trips a 270016-byte secret blob through IndexedDB (bytes now live in a parent-process file in the victim origin's profile storage) and delegates ONLY blob.slice(0,16) to a cross-site frame (site B, separate content process) via a transferred MessagePort. When the (simulated compromised) site-B process issues the normal StreamNeeded request for its 16-byte grant, the parent replies with SlicedInputStreamParams{FileInputStreamParams(fd of the entire victim file), start=0, length=16}; the compromised process ignores the advisory bounds, consumes the inner stream, and recovers all 270016 bytes including the secret never delegated to it - while the legitimate JS read returns exactly 16 bytes (negative control). The verifier independently reproduced this on a verified-clean rebuild (no PoC lines, normal behaviour) followed by a patched rebuild (full deterministic evidence chain). This is an information-disclosure logic flaw at the content-to-parent trust boundary, not memory corruption; no crash is expected.
Build Info
- Branch: master
- Revision: 4395555aa67af0261aac774cf0d418d57a5a549f
- Timestamp: 2026-06-06T00:09:50+00:00
Affected Code
File: dom/file/ipc/PRemoteLazyInputStream.ipdl, line 15
async StreamNeeded(uint64_t aStart, uint64_t aLength) returns (IPCStream? stream);
// aStart/aLength are fully attacker-controlled; the parent actor holds only a UUID,
// so possession of any actor grants access to ANY range of the full backing stream.
File: dom/file/ipc/RemoteLazyInputStreamParent.cpp, line 52-78
mozilla::ipc::IPCResult RemoteLazyInputStreamParent::RecvStreamNeeded(
uint64_t aStart, uint64_t aLength, StreamNeededResolver&& aResolver) {
nsCOMPtr<nsIInputStream> stream;
auto storage = RemoteLazyInputStreamStorage::Get().unwrapOr(nullptr);
if (storage) {
storage->GetStream(mID, aStart, aLength, getter_AddRefs(stream)); // <-- no range/identity authorization: mID is the only credential
}
...
Maybe<IPCStream> ipcStream;
if (NS_WARN_IF(!SerializeIPCStream(stream.forget(), ipcStream,
/* aAllowLazy */ false))) { // <-- eager serialization ships the stream content/capability
return IPC_FAIL(this, "IPCStream serialization failed!");
}
aResolver(ipcStream);
File: dom/file/ipc/RemoteLazyInputStreamStorage.cpp, line 129-166
void RemoteLazyInputStreamStorage::GetStream(const nsID& aID, uint64_t aStart,
uint64_t aLength,
nsIInputStream** aInputStream) {
...
nsresult rv = inputStream->Clone(getter_AddRefs(clonedStream)); // clone of the FULL stored stream
...
// Now it's the right time to apply a slice if needed.
if (aStart > 0 || aLength < UINT64_MAX) { // slice applied per CHILD-REQUESTED range,
clonedStream = // never per the range that was actually granted
new SlicedInputStream(clonedStream.forget(), aStart, aLength);
}
File: xpcom/io/SlicedInputStream.cpp, line 466-487
void SlicedInputStream::Serialize(mozilla::ipc::InputStreamParams& aParams,
uint32_t aMaxSize, uint32_t* aSizeUsed) {
...
SlicedInputStreamParams params;
InputStreamHelper::SerializeInputStream(mInputStream, params.stream(), // <-- serializes the FULL inner stream
aMaxSize, aSizeUsed); // (for nsFileInputStream: dup'd fd of the WHOLE file)
params.start() = mStart; // <-- the slice bounds are mere integers...
params.length() = mLength; // ...enforced only by the (untrusted) recipient's
params.curPos() = mCurPos; // client-side SlicedInputStream::Deserialize/Read
params.closed() = mClosed;
aParams = params;
}
File: dom/file/ipc/RemoteLazyInputStream.cpp, line 1334-1336
IPC::WriteParam(aWriter, actor->StreamID());
IPC::WriteParam(aWriter, mStart); // <-- granted range serialized as plain integers;
IPC::WriteParam(aWriter, mLength); // the parent-side actor never learns or enforces it
The parent-side StreamNeeded handler performs no range authorization and its reply serialization ships the whole backing stream capability with client-side-only bounds.
Exploit Chain
- Victim page (site A, separate Fission process) stores a Blob (16 public bytes + ~270KB secret) in IndexedDB and reads it back; the bytes now live in a parent-process file under the victim origin's profile storage, exposed to site A only as a RemoteLazyInputStream actor (full grant, UUID U1).
- Victim delegates ONLY blob.slice(0, 16) to a cross-site frame (site B, separate content process) via a transferred MessagePort; the parent relay narrows the storage entry to SlicedInputStream(victimFile, 0, 16) under a fresh UUID U2 and grants it to site B's process - 16 bytes is the intended maximum exposure.
- The compromised site-B content process issues the ordinary PRemoteLazyInputStream::StreamNeeded request for its grant.
- RemoteLazyInputStreamParent::RecvStreamNeeded resolves U2, clones the SlicedInputStream, and serializes it: SlicedInputStream::Serialize emits SlicedInputStreamParams{ FileInputStreamParams(dup'd fd of the ENTIRE victim file), start=0, length=16 } - the whole-file OS capability crosses into the compromised process with the limit carried as advisory integers.
- The compromised process deserializes the inner FileInputStreamParams directly and reads the full file: 270016 bytes recovered, secret marker found at offset 16 (269,999 bytes beyond the delegation). It also retains a live read-only fd into the victim origin's IndexedDB blob storage.
- Exfiltration is trivial (the data is now ordinary memory in the attacker process). The same flaw also allows any process holding a range-restricted actor grant to simply request StreamNeeded(0, UINT64_MAX), since the parent never binds the granted range to the actor.
IPC Path
- PRemoteLazyInputStream
- Protocol: PRemoteLazyInputStream
- Parent process: Main
- Child process: Content
- Message flow: Content -> Main: StreamNeeded(aStart, aLength) returns (IPCStream) [reply carries whole-file fd + advisory slice bounds]
Steps to Reproduce
- cd /work/firefox-asan-4 && git apply exploit.patch (compromised-content-process simulation; every hook is gated by XRE_IsContentProcess(), no parent-process behavior is altered)
- Build with mcp build_firefox using mozconfig /work/clauditor-test/mozconfigs/mozconfig.linux.asan.fuzzing
- Run testcase.html via browser_evaluator with firefox_binary /work/firefox-asan-4/obj-firefox-asan/dist/bin/firefox, timeout 60, and prefs: fission.autostart=true, browser.dom.window.dump.enabled=true, dom.security.https_first=false, dom.security.https_first_pbm=false, browser.tabs.remote.useCrossOriginEmbedderPolicy=false, browser.tabs.remote.useCrossOriginOpenerPolicy=false (the COEP/COOP prefs only compensate for the harness HTTP server lacking CORP headers)
- Observe the deterministic RLIS-POC evidence chain in logs.stderr: the attacker-site process receives the 16-byte slice grant, the StreamNeeded reply is 'SlicedInputStreamParams start=0 length=16 innerType=2' (TFileInputStreamParams = whole-file fd), and the ATTACK line shows all 270016 bytes recovered with the secret marker at offset 16, while the legitimate JS read returns exactly 16 bytes (negative control). crashed:false is expected for this non-crash logic flaw.
Security Impact
- Severity: Moderate
- Attacker capability: A compromised content process that possesses any sliced view of a parent-held stream (IndexedDB-backed blob, user-picked File, OPFS-backed file, etc.) can read the ENTIRE backing data rather than the delegated window, and receives a live read-only OS file descriptor into the profile's blob storage (for file-backed entries). This defeats the slice-narrowing confinement the parent relay code deliberately implements and violates Fission cross-origin data-delegation expectations: in the demonstrated scenario a cross-site process delegated 16 bytes recovered a victim origin's full 270KB IndexedDB-backed blob file.
- Preconditions: Compromised content process (renderer RCE, the standard sandbox-escape-adjacent model), plus a victim flow that delegates a slice of a parent-backed Blob/File to an attacker-reachable origin (e.g. chunked-upload widgets, file-preview/share services using blob.slice + postMessage/MessagePort). Given those, full backing-file disclosure is deterministic.
ASAN Report
Not applicable - this is an information-disclosure logic/authorization flaw, not memory corruption; the run exits cleanly (crashed:false, ffpuppet exit -15/CLOSED at harness timeout). Evidence captured from the ASAN-build run (logs.stderr):
RLIS-POC pid=2061357: IPCRead FULL grant id={69953dc7-2b6c-4a3c-a63c-558d08bc8608} start=0
RLIS-STATUS(top): IDB round-trip done; parent-backed blob size=270016
RLIS-STATUS(top): posted 16-byte slice via MessagePort
RLIS-POC pid=2061405: IPCRead FULL grant id={8d807daa-8dba-4bfe-bcca-f88813bf97f1} start=0
RLIS-STATUS(frame): received sliced blob over MessagePort, size=16
RLIS-POC pid=2061405: StreamNeeded reply = SlicedInputStreamParams start=0 length=16 innerType=2
RLIS-POC pid=2061405: ATTACK consumed ENTIRE inner stream behind the 16-byte slice -> 270016 bytes; secret marker offset=16; bytes[16..48]='TOPSECRET_PARENT_ONLY_DATA_TOPSE'
RLIS-STATUS(frame): JS read of slice -> 16 bytes: 'AAAAAAAAAAAAAAAA'
Verifier negative control: identical run on a verified-clean rebuild (libxul free of RLIS-POC strings) produced no PoC lines and normal 16-byte behaviour; patched rebuild deterministically reproduced the leak (attacker pid 2082901, 270016 bytes recovered).
| Reporter | ||
Comment 1•23 days ago
|
||
| Reporter | ||
Comment 2•23 days ago
|
||
| Reporter | ||
Comment 3•23 days ago
|
||
| Reporter | ||
Comment 4•23 days ago
|
||
| Reporter | ||
Comment 5•23 days ago
|
||
| Reporter | ||
Comment 6•23 days ago
|
||
| Reporter | ||
Updated•23 days ago
|
| Reporter | ||
Comment 8•23 days ago
|
||
This seems like a sec-low because AIUI, a site needs to give another site a limited view of the data; but the limit can be bypassed.
Comment 10•23 days ago
•
|
||
(In reply to :Gijs (he/him) from comment #9)
I think this is a dupe of bug 2040591?
Yes, this is definitely a dupe, and my comment there and :mccr8 and :dveditz's skepticism also holds. But if a member of our security team thinks this is a security boundary, it's worth discussing further. I've also put this on our agenda to discuss in more detail tomorrow at DOM security bug triage.
:tjr, are you aware of any websites that actually treat blob slicing as a security boundary? The proposed fix on this bug is potentially a massive performance regression, especially since the reality of how we uses processes is that this would not only impact cross-origin uses, but also most same-origin uses. (That said, things can obviously be re-architected, but especially since blobs can be structured serialized and the re-shipping of MessagePorts can easily send already-serialized messages bouncing between same-origin and cross-origin targets before they arrive, it's potentially a non-trivial undertaking.)
In my own experience of working the email/messaging space for web apps and investigating web-compat issues from sites, I've never seen blob-slicing used in anything approaching a situation that could create an accidental information leak. I think a fundamental aspect is that blobs are (indirect) async handles to homogeneous byte-ish data, and where that byte-ish data fundamentally always needs to be decoded before it's useful to pass on. Additionally, most processing will want to be operating in ArrayBuffer-space and once you've retrieved the data in that fashion, it would be weird to then be generating and handing out offsets to the original Blob.
Some specific speculative heterogeneous payload examples that I think are plausible to consider:
- For multipart MIME encoded messages, while Blob slicing might be used within an internal processing pipeline, the individual leaf parts would always be encoded in a way that's not directly useful, and so would need to be decoded before handing off a string, image, binary data, etc.
- For binary serialization formats (gRPC, avro, etc.) there is categorically a deserialization stage before useful data. If the messages are always fully deserialized in-global, that's fine, so the question is how likely the format is to support fully-encapsulated sub-messages that are propagated as bytestreams for consumption elsewhere and then whether that is done by operating in ArrayBuffer-space or by slicing the original Blob. And then the question is whether we think those binary-serialized messages would be sent as-is without subsequent decoding to a potentially hostile origin.
Interesting homogeneous payload possible uses:
- Assume a video file with parts intended for public consumption plus then private parts not intended for private consumption and the public part is intended to be extracted and uploaded somewhere. Blob slicing could be used to stitch together a new video file that is trying to avoid re-encoding. While it seems more likely that new composite Blob would be bounced to disk, either via explicit save or just automatic saving which would flatten the data, I suppose one could imagine a share mechanism based on an iframe and postMessage where the composite blob is serialized. That could potentially be exploitable, and so there's a question of trade-offs here between degrading all blob performance and how likely it is that the hypothetical video share site would directly embed an iframe for a hostile sharing service that the user would use. If there's indirection like the web share APIs, we can of course make sure that we flatten any blobs transferred ourselves.
Comment 11•22 days ago
|
||
My skepticism in bug 2040591 included the caveat "If this somehow affected blob slices that one origin's web content sent to another then that could be a same-origin-policy violation". The outcome in this testcase is a site-isolation violation. Maybe it's not terrible since it requires the receiving end be in a compromised process, but it's at least a sec-want like many of the "incomplete fission" principal validation problems.
Maybe the problem here is that MessagePort.postMessage() needs to make a copies of a blob slice. What if the Blob slice was put in the transfer array? In theory would that mean we have to take the entire blob away from the parent?
Would a regular window.postMessage() have the same problem? Are there other ways for one site to send or share a blob slice or file with another site?
| Reporter | ||
Comment 12•22 days ago
|
||
I think the conclusion is that this is a sec-want and falls into Fission Site Sandboxing, meaning it's not priority (or something we need to hide) but something we would want to fix under that banner. I'm still tempted to add some sort of comments in the code to head off AI bug hunters who might file more dupes to this, but we can see if it happens again.
Comment 13•21 days ago
|
||
One interesting thing to note is that the blob slice algorithm explicitly does use the awkward phrasing "S refers to span consecutive bytes from blob’s associated byte sequence, beginning with the byte at byte-order position relativeStart" which potentially implies some internal reuse, but the rest of the spec defining the state and serialization/deserialization does not do anything with that, so there's a fair amount of implementation latitude there. Which is fine because blobs are immutable and conceptually (and spec-wise) snapshotted at the moment of creation, although there's potentially some implementation wiggle room there since they're async.
(In reply to Daniel Veditz [:dveditz] from comment #11)
What if the Blob slice was put in the transfer array? In theory would that mean we have to take the entire blob away from the parent?
Blobs are not transferable and so have no transfer semantics; technically it's because they lack a Detached slot that we'll throw in the serialization stage although for reference purposes, the lack of [Transferable] on the WebIDL is the key marker.
Would a regular window.postMessage() have the same problem? Are there other ways for one site to send or share a blob slice or file with another site?
All postMessage implementations use structured serialization under the hood, and all happily will copy/duplicate references to blobs. Blobs are also supported by StructuredSerializeForStorage used by IndexedDB.
Updated•17 days ago
|
Comment 14•17 days ago
|
||
This bug has the keyword crash, so its type should be defect.
Updated•16 days ago
|
| Reporter | ||
Comment 15•16 days ago
•
|
||
Tagging tjr specialized targeting run 1 meap 2760e859-b134-4f02-b215-ebfa04919e70
Description
•