CVE-2026-87499
Overview
Background
- `IsRequestHeaderSafe`
- the
services/networkallowlist check that decides whether a renderer-supplied HTTP request header name/value pair may be sent on a network request. - Shared dictionary compression
- a
net-stack feature where responses are compressed against a previously fetched dictionary, negotiated via thedcb(shared Brotli) anddcz(shared Zstd)Accept-Encodingtokens. - `Accept-Encoding` header
- a request header, legitimately set by the media pipeline (e.g.
identity;q=1, *;q=0), that advertises which content encodings the client will accept. - `net::HttpUtil::ValuesIterator`
- a helper that splits a comma-separated header value into individual tokens for per-value inspection.
Root Cause Analysis
IsRequestHeaderSafe in services/network/public/cpp/header_util.cc validated several sensitive request headers but placed no restriction on the encoding tokens a renderer could put in Accept-Encoding, even though shared dictionary compression is meant to be 100% managed by the network service and net stack. The violated invariant is that the renderer must never influence shared-dictionary negotiation, since the network stack alone knows whether a valid dictionary and the feature are actually available. Because the header passed through unchecked, a renderer could inject dcb or dcz tokens and advertise shared dictionary compression on requests where it was not really enabled, an incorrect-authorization condition where an untrusted process drives a decision reserved for a trusted one.
The fix iterates each comma-separated Accept-Encoding value with net::HttpUtil::ValuesIterator and rejects the header (return false) if any token starts with kSharedBrotliContentEncodingName or kSharedZstdContentEncodingName, while still permitting ordinary values like gzip and the media pipeline’s identity;q=1, *;q=0. This restores the boundary by making the trusted network stack the sole party that may add those tokens.
Accept-Encoding value wholesale, letting it advertise shared-dictionary encodings that only the network stack is authorized to negotiate; the fix closes this by explicitly filtering dcb/dcz tokens out of renderer-provided Accept-Encoding headers.Attack Path
- Compromised renderer
An attacker who controls a renderer process crafts a network request through a path that lets it set the
Accept-Encodingrequest header. - Inject dictionary tokens
The renderer inserts
dcbordcz(e.g.gzip, dcbordcz;q=1) into the value, whichIsRequestHeaderSafepreviously accepted unchanged. - Advertise unenabled compression The request goes out advertising shared dictionary compression even though the network stack did not enable it or select a dictionary.
- Server responds accordingly A server may then negotiate and return a shared-dictionary-compressed response outside the state the trusted net stack expects.
Impact Assessment
dcb/dcz) into outgoing requests, driving a negotiation the network service is supposed to solely control. The precondition is a renderer able to influence the Accept-Encoding request header on a request handled by the network service. The exposure is an incorrect-authorization/logic flaw in the Network component rather than a demonstrated memory-safety primitive from this diff alone.Files Changed
services/network/public/cpp/header_util_unittest.ccservices/network/public/cpp/header_util.cc
Audit Directions
- Renderer-controlled header allowlistsAudit every header-safety predicate (like
IsRequestHeaderSafe) for encodings, tokens, or directives that a trusted layer negotiates but the allowlist forwards verbatim from an untrusted process. - Comma-separated header parsingVerify that checks on list-valued headers (
Accept-Encoding,Connection,Transfer-Encoding) inspect each token with a proper iterator rather than the raw string, since a single-token match is easy to bypass with prefixes orq-values likedcz;q=1. - Trust-boundary-managed featuresReview features documented as fully owned by the network/net stack (shared dictionaries, compression, priority hints) to confirm no renderer-supplied input can toggle or advertise them.
Patch
From 4568e184333b8b92b07c135f11b10b3d811d8f36 Mon Sep 17 00:00:00 2001
From: Patrick Meenan <pmeenan@chromium.org>
Date: Thu, 27 Aug 2026 18:42:10 -0700
Subject: [PATCH] Block renderer-provided dcb or dcz Accept-Encoding headers
Dictionary compression is 100% handled in the network service and net
stack. This makes sure that renderer's don't accidentally try to
advertise dictionary compression when it's not really enabled.
Bug: 553118043
Change-Id: If9cdcf100520e0f74216daa80dc948e0f397748f
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8300933
Commit-Queue: Patrick Meenan <pmeenan@chromium.org>
Reviewed-by: Tsuyoshi Horo <horo@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1687719}
---
diff --git a/services/network/public/cpp/header_util.cc b/services/network/public/cpp/header_util.cc
index 402042a..a06abd25 100644
--- a/services/network/public/cpp/header_util.cc
+++ b/services/network/public/cpp/header_util.cc
@@ -18,6 +18,7 @@
#include "net/http/http_response_headers.h"
#include "net/http/http_status_code.h"
#include "net/http/http_util.h"
+#include "net/shared_dictionary/shared_dictionary_constants.h"
#include "services/network/public/cpp/cors/cors.h"
#include "services/network/public/cpp/features.h"
#include "services/network/public/mojom/url_response_head.mojom.h"
@@ -75,6 +76,26 @@
return false;
}
+ // The Accept-Encoding header can be set by the media pipeline (e.g.
+ // "identity;q=1, *;q=0"), but must not be used to negotiate shared
+ // dictionary compression (dcb, dcz) which is managed by the network stack.
+ if (base::EqualsCaseInsensitiveASCII(
+ key, net::HttpRequestHeaders::kAcceptEncoding)) {
+ net::HttpUtil::ValuesIterator encodings(value, ',');
+ while (encodings.GetNext()) {
+ if (base::StartsWith(
+ encodings.value(),
+ net::shared_dictionary::kSharedBrotliContentEncodingName,
+ base::CompareCase::INSENSITIVE_ASCII) ||
+ base::StartsWith(
+ encodings.value(),
+ net::shared_dictionary::kSharedZstdContentEncodingName,
+ base::CompareCase::INSENSITIVE_ASCII)) {
+ return false;
+ }
+ }
+ }
+
// The Connection header is a comma-separated list of tokens. Per RFC 9110
// section 7.6.1, intermediaries treat each listed token as the name of a
// header to remove before forwarding, so only allow the connection-management
diff --git a/services/network/public/cpp/header_util_unittest.cc b/services/network/public/cpp/header_util_unittest.cc
index 9d2adf4..f4635e2 100644
--- a/services/network/public/cpp/header_util_unittest.cc
+++ b/services/network/public/cpp/header_util_unittest.cc
@@ -8,6 +8,7 @@
#include "base/strings/stringprintf.h"
#include "net/http/http_request_headers.h"
#include "net/http/http_response_headers.h"
+#include "net/shared_dictionary/shared_dictionary_constants.h"
#include "services/network/public/cpp/features.h"
#include "testing/gtest/include/gtest/gtest.h"
@@ -28,6 +29,15 @@
{"Upgrade", "webbedsocket", false},
{"hOsT", "foo.test", false},
+ {net::HttpRequestHeaders::kAcceptEncoding, "gzip", true},
+ {net::HttpRequestHeaders::kAcceptEncoding, "identity;q=1, *;q=0", true},
+ {net::HttpRequestHeaders::kAcceptEncoding, "gzip, identity;q=1, *;q=0",
+ true},
+ {net::HttpRequestHeaders::kAcceptEncoding, "dcb", false},
+ {net::HttpRequestHeaders::kAcceptEncoding, "dcz", false},
+ {net::HttpRequestHeaders::kAcceptEncoding, "gzip, dcb", false},
+ {net::HttpRequestHeaders::kAcceptEncoding, "dcz;q=1", false},
+
{net::HttpRequestHeaders::kConnection, "Upgrade", false},
{net::HttpRequestHeaders::kConnection, "Close", true},
{net::HttpRequestHeaders::kConnection, "keep-alive", true},
@@ -92,6 +102,15 @@
{net::HttpRequestHeaders::kTransferEncoding, "gzip", false},
{"Set-Cookie", "foo=bar", false},
+ {net::HttpRequestHeaders::kAcceptEncoding, "gzip", true},
+ {net::HttpRequestHeaders::kAcceptEncoding, "identity;q=1, *;q=0", true},
+ {net::HttpRequestHeaders::kAcceptEncoding, "gzip, identity;q=1, *;q=0",
+ true},
+ {net::HttpRequestHeaders::kAcceptEncoding, "dcb", false},
+ {net::HttpRequestHeaders::kAcceptEncoding, "dcz", false},
+ {net::HttpRequestHeaders::kAcceptEncoding, "gzip, dcb", false},
+ {net::HttpRequestHeaders::kAcceptEncoding, "dcz;q=1", false},
+
{net::HttpRequestHeaders::kConnection, "Upgrade", false},
{net::HttpRequestHeaders::kConnection, "Close", true},
{net::HttpRequestHeaders::kConnection, "keep-alive", true},
Regression Test / PoC
diff --git a/services/network/public/cpp/header_util_unittest.cc b/services/network/public/cpp/header_util_unittest.cc
index 9d2adf4..f4635e2 100644
--- a/services/network/public/cpp/header_util_unittest.cc
+++ b/services/network/public/cpp/header_util_unittest.cc
@@ -8,6 +8,7 @@
#include "base/strings/stringprintf.h"
#include "net/http/http_request_headers.h"
#include "net/http/http_response_headers.h"
+#include "net/shared_dictionary/shared_dictionary_constants.h"
#include "services/network/public/cpp/features.h"
#include "testing/gtest/include/gtest/gtest.h"
@@ -28,6 +29,15 @@
{"Upgrade", "webbedsocket", false},
{"hOsT", "foo.test", false},
+ {net::HttpRequestHeaders::kAcceptEncoding, "gzip", true},
+ {net::HttpRequestHeaders::kAcceptEncoding, "identity;q=1, *;q=0", true},
+ {net::HttpRequestHeaders::kAcceptEncoding, "gzip, identity;q=1, *;q=0",
+ true},
+ {net::HttpRequestHeaders::kAcceptEncoding, "dcb", false},
+ {net::HttpRequestHeaders::kAcceptEncoding, "dcz", false},
+ {net::HttpRequestHeaders::kAcceptEncoding, "gzip, dcb", false},
+ {net::HttpRequestHeaders::kAcceptEncoding, "dcz;q=1", false},
+
{net::HttpRequestHeaders::kConnection, "Upgrade", false},
{net::HttpRequestHeaders::kConnection, "Close", true},
{net::HttpRequestHeaders::kConnection, "keep-alive", true},
@@ -92,6 +102,15 @@
{net::HttpRequestHeaders::kTransferEncoding, "gzip", false},
{"Set-Cookie", "foo=bar", false},
+ {net::HttpRequestHeaders::kAcceptEncoding, "gzip", true},
+ {net::HttpRequestHeaders::kAcceptEncoding, "identity;q=1, *;q=0", true},
+ {net::HttpRequestHeaders::kAcceptEncoding, "gzip, identity;q=1, *;q=0",
+ true},
+ {net::HttpRequestHeaders::kAcceptEncoding, "dcb", false},
+ {net::HttpRequestHeaders::kAcceptEncoding, "dcz", false},
+ {net::HttpRequestHeaders::kAcceptEncoding, "gzip, dcb", false},
+ {net::HttpRequestHeaders::kAcceptEncoding, "dcz;q=1", false},
+
{net::HttpRequestHeaders::kConnection, "Upgrade", false},
{net::HttpRequestHeaders::kConnection, "Close", true},
{net::HttpRequestHeaders::kConnection, "keep-alive", true},
Original Bug Report
ORB bypass and cross-origin leak via forged Compression Dictionary Transport headers
Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: A compromised renderer can attach Compression Dictionary Transport negotiation headers (Available-Dictionary, Dictionary-ID, and Accept-Encoding containing dcb/dcz) to cross-origin credentialed no-cors requests because they are not rejected at the Network Service boundary. When a server responds with delta-compressed content, the network stack treats the encoding as unknown and passes the raw stream through to Opaque Response Blocking (ORB) undecoded. ORB fails to match HTML/JSON signatures on the compressed binary bytes and fails open, allowing the compromised renderer to receive and decompress cross-origin responses.
Affected files:
services/network/public/cpp/header_util.ccservices/network/cors/cors_url_loader_factory.ccservices/network/orb/orb_impl.ccnet/filter/filter_source_stream.ccnet/shared_dictionary/shared_dictionary_network_transaction.cc
Estimated timestamp from git blame: 2024-05-23
Summary
A compromised renderer can potentially bypass Opaque Response Blocking (ORB) and read sensitive cross-origin responses by injecting Compression Dictionary Transport (RFC 9842) request headers (Available-Dictionary, Dictionary-ID, and Accept-Encoding: ..., dcb, dcz) into cross-origin no-cors requests. This leads to a cross-origin data disclosure / Site Isolation bypass.
Technical Details
-
Boundary Validation Gap (
services/network/public/cpp/header_util.cc):CorsURLLoaderFactory::IsValidRequestvalidates request headers viaAreRequestHeadersSafeandContainsForbiddenSecurityHeader(services/network/cors/cors_url_loader_factory.cc:904-918).ContainsForbiddenSecurityHeaderonly checksSec-prefixed headers.AreRequestHeadersSafechecks againstkUnsafeHeaders(services/network/public/cpp/header_util.cc:38-68), which does not includeAccept-Encoding,Available-Dictionary, orDictionary-ID. Consequently, a compromised renderer can attach these headers to any subresource request. -
Header Forwarding and Flag Absence (
net/http/http_request_headers.cc,services/network/cors/cors_url_loader.cc):HttpRequestHeaders::SetAcceptEncodingIfMissing(net/http/http_request_headers.cc:238-239) early-returns whenAccept-Encodingis already set, preserving the renderer-supplied header.- For cross-origin
no-corsrequests,CorsURLLoader::StartRequest(services/network/cors/cors_url_loader.cc:364-376) does not attachnet::LOAD_CAN_USE_SHARED_DICTIONARY. As a result,SharedDictionaryNetworkTransaction::Start(net/shared_dictionary/shared_dictionary_network_transaction.cc:142-145) executes in pure passthrough mode without fetching or registering local dictionaries.
-
Undecoded Stream Passthrough (
net/filter/filter_source_stream.cc): When the target server responds withContent-Encoding: dcb(ordcz),FilterSourceStream::ParseEncodingType(net/filter/filter_source_stream.cc:90-107) encounters an unlisted encoding inkEncodingMapand returnsSourceStreamType::kUnknown.FilterSourceStream::GetContentEncodingTypesreturns an empty vector (lines 127-132), causingURLRequestHttpJob::SetUpSourceStream(net/url_request/url_request_http_job.cc:1540-1568) to return the raw, undecoded network stream. -
ORB Fail-Open on Compressed Bytes (
services/network/orb/orb_impl.cc):- For responses labeled
text/htmlorapplication/jsonwithoutX-Content-Type-Options: nosniff,OpaqueResponseBlockingAnalyzer::Init(services/network/orb/orb_impl.cc:317-325) returnsDecision::kSniffMore. - The raw stream starts with dictionary compression magic bytes (
0xFF 0x44 0x43 0x42fordcb/0x5E 0x2A 0x4D 0x18fordcz). Sniffers inservices/network/orb/orb_sniffers.ccfail to find HTML tags or JSON braces and returnkNo. - At the end of the 1024-byte sniffing window,
HandleEndOfSniffableResponseBodyexecutes its fail-open fallback (services/network/orb/orb_impl.cc:472-474), returningDecision::kAllow. - The raw body pipe is delivered to the renderer via
CorsURLLoader::OnReceiveResponse, where theCHECKat line 696 succeeds becausedid_use_shared_dictionaryis false.
- For responses labeled
Potential Trigger Steps
(Note: These are suggested theoretical steps derived from static code analysis; automated tooling does not currently have live execution capability.)
- Compromise a renderer process hosting an attacker-controlled origin (
https://attacker.example). - Identify a target endpoint on
https://victim.example/accountthat supports Compression Dictionary Transport (RFC 9842) for dynamic responses and does not sendX-Content-Type-Options: nosnifforCross-Origin-Resource-Policy. - Anonymously retrieve the victim origin’s public dictionary asset (
dict.dat), compute its SHA-256 hash, and format it as an RFC 8941 Structured Field byte sequence (:<base64_sha256>:). - From the compromised renderer, send a Mojo IPC
URLLoaderFactory::CreateLoaderAndStartrequest with:url = https://victim.example/accountmode = mojom::RequestMode::kNoCorscredentials_mode = mojom::CredentialsMode::kIncludeclient_side_content_decoding_enabled = falseheaders = { 'Accept-Encoding': 'gzip, br, zstd, dcb, dcz', 'Available-Dictionary': ':<base64_sha256>:' }
- The Network Service forwards the forged headers verbatim along with ambient credentials (
SameSite=Nonecookies). - The victim server returns
Content-Encoding: dcbdelta-compressed against the public dictionary. - The Network Service passes the raw stream through undecoded, ORB fails open to
kAllow, and the raw compressed stream is piped to the renderer. - The renderer decompresses the stream locally using
dict.dat, disclosing the cross-origin plaintext response.
Suggested Fix
- Add
Available-DictionaryandDictionary-IDtokUnsafeHeadersinservices/network/public/cpp/header_util.cc(or sanitize them inCorsURLLoaderFactory::IsValidRequest). - Restrict renderer-specified
Accept-Encodingheaders that include dictionary encodings (dcb/dcz) unless authorized by the Network Service. - Reject or treat as an error unexpected
Content-Encoding: dcb/dczresponses whendid_send_available_dictionary/LOAD_CAN_USE_SHARED_DICTIONARYis not set for the transaction.
Evaluated with Chrome root at commit: c6718ebbf2c677c6f63691e28c7801c563f6b19c
Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:
- If you are familiar with the severity guidelines, you may adjust the severity.
- If this is a false positive, and there’s no work to be done, please close as WAI.
- If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.
Data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.