CVE-2026-87551
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifservices/network/cors/cors_url_loader.cc |
modified | |
TEST_Fservices/network/cors/cors_url_loader_shared_dictionary_unittest.cc |
modified |
Files Changed
services/network/cors/cors_url_loader.ccservices/network/cors/cors_url_loader_shared_dictionary_unittest.cc
Patch
From e08e3ee28570e634bfa6e3a752fcaf9d8aa9f0d7 Mon Sep 17 00:00:00 2001
From: Patrick Meenan <pmeenan@chromium.org>
Date: Tue, 28 Jul 2026 18:57:52 -0700
Subject: [PATCH] net: Prevent dictionary writes for cert errors
Don't persist compression dictionaries in the case of a TLS certificate
error.
Bug: 540070236
Change-Id: Idd0ba9cd844bad97f3757a840895a63944f1e778
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8160125
Commit-Queue: Patrick Meenan <pmeenan@chromium.org>
Reviewed-by: Tsuyoshi Horo <horo@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1669942}
---
diff --git a/services/network/cors/cors_url_loader.cc b/services/network/cors/cors_url_loader.cc
index 8a7b64d..313c1cb 100644
--- a/services/network/cors/cors_url_loader.cc
+++ b/services/network/cors/cors_url_loader.cc
@@ -20,6 +20,7 @@
#include "base/types/optional_util.h"
#include "net/base/load_flags.h"
#include "net/base/request_priority.h"
+#include "net/cert/cert_status_flags.h"
#include "net/cookies/cookie_partition_key.h"
#include "net/cookies/cookie_setting_override.h"
#include "net/cookies/cookie_util.h"
@@ -629,7 +630,8 @@
std::optional<std::string> use_as_dictionary_header = GetHeaderString(
*response_head, shared_dictionary::kUseAsDictionaryHeaderName);
- if (use_as_dictionary_header) {
+ if (use_as_dictionary_header &&
+ !net::IsCertStatusError(response_head->cert_status)) {
base::expected<scoped_refptr<SharedDictionaryWriter>,
mojom::SharedDictionaryError>
writer_or_error = SharedDictionaryStorage::MaybeCreateWriter(
diff --git a/services/network/cors/cors_url_loader_shared_dictionary_unittest.cc b/services/network/cors/cors_url_loader_shared_dictionary_unittest.cc
index 61fb57f..f9080a9 100644
--- a/services/network/cors/cors_url_loader_shared_dictionary_unittest.cc
+++ b/services/network/cors/cors_url_loader_shared_dictionary_unittest.cc
@@ -4,7 +4,9 @@
#include "base/feature_list.h"
#include "mojo/public/cpp/system/data_pipe_utils.h"
+#include "net/cert/cert_status_flags.h"
#include "net/cookies/site_for_cookies.h"
+#include "net/http/http_response_headers.h"
#include "services/network/cors/cors_url_loader.h"
#include "services/network/cors/cors_url_loader_test_util.h"
#include "services/network/network_context.h"
@@ -196,6 +198,31 @@
CheckDictionaryInStorage(/*expect_exists=*/true);
}
+TEST_F(CorsURLLoaderSharedDictionaryTest, CertStatusErrorPreventStorage) {
+ ResetFactory();
+ ResourceRequest request = CreateResourceRequest();
+ request.mode = mojom::RequestMode::kSameOrigin;
+ CreateLoaderAndStart(request);
+ RunUntilCreateLoaderAndStartCalled();
+
+ CreateDataPipeAndWriteTestData();
+ auto response_head = mojom::URLResponseHead::New();
+ response_head->headers = base::MakeRefCounted<net::HttpResponseHeaders>(
+ "HTTP/1.1 200 OK\n"
+ "Use-As-Dictionary: match=\"/path*\"\n"
+ "Cache-Control: max-age=2592000\n");
+ response_head->cert_status = net::CERT_STATUS_AUTHORITY_INVALID;
+ NotifyLoaderClientOnReceiveResponse(std::move(response_head),
+ std::move(consumer_handle_));
+ NotifyLoaderClientOnComplete(net::OK);
+ producer_handle_.reset();
+
+ RunUntilComplete();
+ EXPECT_EQ(net::OK, client().completion_status().error_code);
+
+ CheckDictionaryInStorage(/*expect_exists=*/false);
+}
+
TEST_F(CorsURLLoaderSharedDictionaryTest, SameOriginUrlNoCorsModeRequest) {
ResetFactory();
Regression Test / PoC
diff --git a/services/network/cors/cors_url_loader_shared_dictionary_unittest.cc b/services/network/cors/cors_url_loader_shared_dictionary_unittest.cc
index 61fb57f..f9080a9 100644
--- a/services/network/cors/cors_url_loader_shared_dictionary_unittest.cc
+++ b/services/network/cors/cors_url_loader_shared_dictionary_unittest.cc
@@ -4,7 +4,9 @@
#include "base/feature_list.h"
#include "mojo/public/cpp/system/data_pipe_utils.h"
+#include "net/cert/cert_status_flags.h"
#include "net/cookies/site_for_cookies.h"
+#include "net/http/http_response_headers.h"
#include "services/network/cors/cors_url_loader.h"
#include "services/network/cors/cors_url_loader_test_util.h"
#include "services/network/network_context.h"
@@ -196,6 +198,31 @@
CheckDictionaryInStorage(/*expect_exists=*/true);
}
+TEST_F(CorsURLLoaderSharedDictionaryTest, CertStatusErrorPreventStorage) {
+ ResetFactory();
+ ResourceRequest request = CreateResourceRequest();
+ request.mode = mojom::RequestMode::kSameOrigin;
+ CreateLoaderAndStart(request);
+ RunUntilCreateLoaderAndStartCalled();
+
+ CreateDataPipeAndWriteTestData();
+ auto response_head = mojom::URLResponseHead::New();
+ response_head->headers = base::MakeRefCounted<net::HttpResponseHeaders>(
+ "HTTP/1.1 200 OK\n"
+ "Use-As-Dictionary: match=\"/path*\"\n"
+ "Cache-Control: max-age=2592000\n");
+ response_head->cert_status = net::CERT_STATUS_AUTHORITY_INVALID;
+ NotifyLoaderClientOnReceiveResponse(std::move(response_head),
+ std::move(consumer_handle_));
+ NotifyLoaderClientOnComplete(net::OK);
+ producer_handle_.reset();
+
+ RunUntilComplete();
+ EXPECT_EQ(net::OK, client().completion_status().error_code);
+
+ CheckDictionaryInStorage(/*expect_exists=*/false);
+}
+
TEST_F(CorsURLLoaderSharedDictionaryTest, SameOriginUrlNoCorsModeRequest) {
ResetFactory();
Original Bug Report
Lack of SSL certificate validation in Use-As-Dictionary writer path allows MITM persistence
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: The write path for the Use-As-Dictionary response header does not inspect the connection’s SSL certificate status or verify if the certificate is issued by a known root. This potentially allows an active network attacker on a certificate-error HTTPS connection (following an interstitial click-through) to persist arbitrary, origin-keyed dictionary IDs. These IDs will be transmitted back to the legitimate server on subsequent clean, valid TLS connections.
Affected files:
services/network/cors/cors_url_loader.ccservices/network/shared_dictionary/shared_dictionary_storage.cc
Estimated timestamp from git blame: 2023-04-12
Root Cause
When processing a response containing the Use-As-Dictionary header, CorsURLLoader::OnReceiveResponse delegates writer creation to SharedDictionaryStorage::MaybeCreateWriter without inspecting the connection’s ssl_info or certificate status:
// services/network/cors/cors_url_loader.cc:651-667
std::optional<std::string> use_as_dictionary_header = GetHeaderString(
*response_head, shared_dictionary::kUseAsDictionaryHeaderName);
if (use_as_dictionary_header) {
base::expected<scoped_refptr<SharedDictionaryWriter>,
mojom::SharedDictionaryError>
writer_or_error = SharedDictionaryStorage::MaybeCreateWriter(
*use_as_dictionary_header,
request_.shared_dictionary_writer_enabled,
shared_dictionary_storage_.get(), request_.mode, response_tainting_,
request_.url, response_head->request_time,
response_head->response_time, *response_head->headers,
response_head->was_fetched_via_cache,
base::BindOnce(
&SharedDictionaryAccessChecker::CheckAllowedToWriteAndReport, ...));
Inside SharedDictionaryStorage::MaybeCreateWriter (services/network/shared_dictionary/shared_dictionary_storage.cc), checks are performed on the request mode, writer-enabled state, secure-context storage, and response tainting, but the function does not inspect connection security flags or ssl_info.
By contrast, other persistent origin-keyed network state features prevent registration on certificate-error connections. For example, Strict Transport Security (HSTS) and Device Bound Session Credentials (DBSC) both explicitly abort processing if a certificate error is present:
// net/url_request/url_request_http_job.cc:1168 and :1192
if (!ssl_info.is_valid() || IsCertStatusError(ssl_info.cert_status)) {
return;
}
Additionally, the shared dictionary read path carries a known-root certificate constraint (net/shared_dictionary/shared_dictionary_network_transaction.cc:250-255), which is checked when utilizing a registered dictionary on subsequent connections. However, because the write path is not gated similarly, an invalid or untrusted certificate session can successfully populate on-disk persistent dictionary state.
Potential Impact
An active network attacker who successfully convinces a user to click through an SSL/TLS interstitial warning on https://victim.example can write and persist an arbitrary compression dictionary payload and a custom Dictionary-ID (up to 1024 bytes) for the victim’s origin.
On subsequent navigations to the legitimate https://victim.example over a clean network with a valid root certificate, the browser’s SharedDictionaryNetworkTransaction will retrieve the registered dictionary and attach the attacker-chosen Dictionary-ID header to all matching outgoing requests. This violates the security boundary that prevents untrusted certificate connections from setting persistent origin-keyed network state.
Suggested Reproduction Steps (Potential)
Note: Our tooling does not currently have the capability to run code directly, so these are potential steps to illustrate the behavior:
- On an attacker-controlled network, intercept requests to
https://victim.examplewith a self-signed certificate. - In Chrome, navigate to
https://victim.exampleand click “Proceed” on the SSL interstitial warning. - Serve a
200 OKresponse with the headerUse-As-Dictionary: match="/*", id="MITM-PLANTED-12345", accompanied by a valid dictionary body and cache headers. - Close the browser, move the client to a clean network, and restart Chrome.
- Navigate to the legitimate
https://victim.exampleand observe that the outgoing request headers containDictionary-ID: "MITM-PLANTED-12345"over the genuine secure connection.
Suggested Fix
Ensure that dictionaries are not registered on connections with certificate errors. In CorsURLLoader::OnReceiveResponse (or inside SharedDictionaryStorage::MaybeCreateWriter), evaluate response_head->cert_status or inspect the response’s ssl_info to ensure there are no certificate errors prior to writer creation:
if (response_head->ssl_info && net::IsCertStatusError(response_head->ssl_info->cert_status)) {
// Reject dictionary writer creation
}
Evaluated with Chrome root at commit: 94d9235ebe3b7276e5284f0dc5d55577ff949908
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.