CVE-2024-10461
Overview
Background
- `multipart/x-mixed-replace`
- A streaming response type in which the server sends a sequence of body parts, each replacing the previously rendered one, commonly used for server-push and live-updating resources.
- `nsMultiMixedConv`
- Firefox’s stream converter that parses a
multipart/x-mixed-replacestream and dispatches each part as its own channel (nsPartChannel) to upper layers. - `Content-Disposition: attachment`
- An HTTP response header directive instructing the browser to download the resource rather than render it inline, a common defense against rendering attacker-controlled content in a document context.
- `SetContentDisposition`
- The
nsPartChannelmethod that records the disposition value later consulted to decide whether a part is displayed inline or forced to download.
Root Cause Analysis
When nsMultiMixedConv processed a multipart/x-mixed-replace response, the top-level response header Content-Disposition was read during OnStartRequest for other headers (such as Content-Security-Policy) but the disposition itself was never captured for the outer document. In SendStart, each part channel had its disposition set via mPartChannel->SetContentDisposition(mContentDisposition), where mContentDisposition reflected only per-part heading data and defaulted to empty, so an attachment disposition present on the root response was silently dropped. The violated invariant is that a server’s Content-Disposition: attachment on the response must force a download and prevent inline rendering; because the value never propagated to the part channels, attacker-supplied parts were rendered inline as active documents instead of being downloaded.
The fix records the root response’s disposition into a new mRootContentDisposition member during OnStartRequest, then in SendStart prefers that value (if (!mRootContentDisposition.IsEmpty())) over the per-part value when setting the part channel’s disposition. This restores the invariant so a top-level attachment directive is respected across every part of the multipart stream.
Content-Disposition header to the individual part channels, letting it be overwritten by an empty per-part value; the fix captures the root disposition in mRootContentDisposition and applies it in preference so attachment is honored.Attack Path
- Serve a multipart response
An attacker controls or influences a server endpoint that returns a
multipart/x-mixed-replaceresponse withContent-Disposition: attachmentset on the top-level response header. - Header is captured but ignored
nsMultiMixedConv::OnStartRequestreads other root headers but never stores the disposition, so the intended download directive is lost before parts are dispatched. - Part channels get empty disposition
In
nsMultiMixedConv::SendStart,SetContentDispositionis called with the emptymContentDisposition, so no part is flagged for download. - Inline rendering as a document Each attacker-controlled part is treated as a displayable top-level document rather than an attachment, allowing HTML/script content to execute in the origin’s context.
- XSS execution
The rendered active content runs as script in the loading context, achieving cross-site scripting that the
attachmentdisposition was meant to prevent.
Impact Assessment
multipart/x-mixed-replace response that should have been forced to download. The precondition is that the server response carries Content-Disposition: attachment at the top level while using the multipart content type, and that a victim loads that response in the browser. The impact is rated moderate because it requires a multipart response configuration and defeats a download-based mitigation rather than granting unconditional code execution.Files Changed
netwerk/streamconv/converters/nsMultiMixedConv.cppnetwerk/streamconv/converters/nsMultiMixedConv.h
Audit Directions
- Header propagation across convertersReview stream converters and channel-splitting code (like
nsMultiMixedConv) for security-relevant response headers (Content-Disposition,Content-Security-Policy,X-Content-Type-Options) that are read for the root but not carried onto derived per-part or sub-resource channels. - Empty-value overwritesLook for places where a security directive is unconditionally set from a per-item field that may be empty, silently clobbering a stronger value inherited from a parent context.
- Multipart and streaming pathsAudit other multipart, chunked, or push-style response handlers to confirm that download-forcing and rendering-suppression directives are honored on every emitted part, not just the first or the outer response.
Patch
diff --git a/netwerk/streamconv/converters/nsMultiMixedConv.cpp b/netwerk/streamconv/converters/nsMultiMixedConv.cpp
index e1ccfda303c..5c4ea72a86e 100644
--- a/netwerk/streamconv/converters/nsMultiMixedConv.cpp
+++ b/netwerk/streamconv/converters/nsMultiMixedConv.cpp
@@ -474,6 +474,12 @@ nsMultiMixedConv::OnStartRequest(nsIRequest* request) {
if (NS_SUCCEEDED(rv)) {
mRootContentSecurityPolicy = csp;
}
+ nsCString contentDisposition;
+ rv = httpChannel->GetResponseHeader("content-disposition"_ns,
+ contentDisposition);
+ if (NS_SUCCEEDED(rv)) {
+ mRootContentDisposition = contentDisposition;
+ }
} else {
// try asking the channel directly
rv = mChannel->GetContentType(contentType);
@@ -850,7 +856,11 @@ nsresult nsMultiMixedConv::SendStart() {
rv = mPartChannel->SetContentLength(mContentLength);
if (NS_FAILED(rv)) return rv;
- mPartChannel->SetContentDisposition(mContentDisposition);
+ if (!mRootContentDisposition.IsEmpty()) {
+ mPartChannel->SetContentDisposition(mRootContentDisposition);
+ } else {
+ mPartChannel->SetContentDisposition(mContentDisposition);
+ }
// Each part of a multipart/replace response can be used
// for the top level document. We must inform upper layers
diff --git a/netwerk/streamconv/converters/nsMultiMixedConv.h b/netwerk/streamconv/converters/nsMultiMixedConv.h
index 4f773a69eb6..694150d69c2 100644
--- a/netwerk/streamconv/converters/nsMultiMixedConv.h
+++ b/netwerk/streamconv/converters/nsMultiMixedConv.h
@@ -151,15 +151,17 @@ class nsMultiMixedConv : public nsIStreamConverter {
nsCOMPtr<nsIStreamListener> mFinalListener; // this guy gets the converted
// data via his OnDataAvailable()
- nsCOMPtr<nsIChannel>
- mChannel; // The channel as we get in in OnStartRequest call
- RefPtr<nsPartChannel> mPartChannel; // the channel for the given part we're
- // processing. one channel per part.
+ // The channel as we get it in OnStartRequest call
+ nsCOMPtr<nsIChannel> mChannel;
+ // the channel for the given part we're
+ // processing. one channel per part.
+ RefPtr<nsPartChannel> mPartChannel;
nsCOMPtr<nsISupports> mContext;
nsCString mContentType;
nsCString mContentDisposition;
nsCString mContentSecurityPolicy;
nsCString mRootContentSecurityPolicy;
+ nsCString mRootContentDisposition;
uint64_t mContentLength{UINT64_MAX};
uint64_t mTotalSent{0};