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.ccservices/network/public/cpp/header_util_unittest.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 <[email protected]> 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 <[email protected]> Reviewed-by: Tsuyoshi Horo <[email protected]> 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},