Low watch CVSS 4.3 chrome Cross Origin 📄 Reporter bug report 🔧 Commit mapped

Overview

Low
Severity
4.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactImproper certificate validation in CORS
DescriptionImproper certificate validation in CORS
ComponentCORS
Sub-componentNet
Bug ClassCross Origin
Action tierwatch — logic/policy bug — security-relevant, no memory-unsafety
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker540070236
Fix commite08e3ee28570 (chromium/src) +30/-1
CWECWE-295
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:L/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-07-28 (per vendor advisory)
Fix landed2026-07-29 (public git)
Disclosed2026-09-08
Patch gap41 days — fix public in source before the advisory

Background

Compression dictionary transport
a mechanism where a response bearing a Use-As-Dictionary header is persisted so its body can serve as a Brotli/Zstd dictionary that compresses future responses from the same origin/scope.
`SharedDictionaryWriter`
the object, obtained from SharedDictionaryStorage::MaybeCreateWriter, that captures a response body into on-disk shared-dictionary storage for later reuse.
`net::IsCertStatusError`
a helper in net/cert/cert_status_flags.h that returns true when a response’s cert_status bitfield carries any fatal TLS certificate error (e.g. CERT_STATUS_AUTHORITY_INVALID).
`cert_status`
a field on mojom::URLResponseHead recording the TLS certificate validation result for the connection that produced the response.

Root Cause Analysis

In services/network/cors/cors_url_loader.cc, the response-handling path checked only whether a Use-As-Dictionary header was present (if (use_as_dictionary_header)) before calling SharedDictionaryStorage::MaybeCreateWriter and persisting the response body as a shared compression dictionary. That guard never consulted response_head->cert_status, so a response delivered over a connection whose certificate failed validation was still eligible to write a persistent dictionary. This violated the invariant that data from a connection with an untrusted/invalid certificate must not be persisted as trusted, reusable state.

The fix tightens the condition to use_as_dictionary_header && !net::IsCertStatusError(response_head->cert_status), so any fatal certificate error now short-circuits the write and no dictionary is stored. Because IsCertStatusError is evaluated as part of the same if, the MaybeCreateWriter call and the subsequent persistence are simply skipped on the cert-error path, matching the added CertStatusErrorPreventStorage test that sets CERT_STATUS_AUTHORITY_INVALID and asserts the dictionary is absent from storage.

Key insight
The dictionary-write decision was gated solely on the presence of the Use-As-Dictionary header and ignored the connection’s certificate validity; the fix adds a !net::IsCertStatusError(response_head->cert_status) term so responses over certificate-invalid connections can no longer poison persistent shared-dictionary storage.

Attack Path

  1. Position on the connection An attacker able to intercept or serve traffic for a target origin (e.g. a network/MITM attacker or a proxy) produces a response over a TLS connection whose certificate fails validation, yielding a set cert_status such as CERT_STATUS_AUTHORITY_INVALID.
  2. Serve a dictionary directive The attacker-controlled response includes a Use-As-Dictionary header (with a match scope) and cacheable directives, so the pre-patch guard if (use_as_dictionary_header) passes despite the certificate error.
  3. Persist the poisoned dictionary SharedDictionaryStorage::MaybeCreateWriter returns a SharedDictionaryWriter and the attacker-supplied body is written into persistent shared-dictionary storage as trusted state.
  4. Reuse against future requests Later requests within the dictionary’s match scope select the poisoned dictionary for decompression, letting attacker-controlled bytes influence how subsequent responses are interpreted (inferred), which is the improper-certificate-validation condition the fix closes.

Impact Assessment

An attacker who can cause a certificate-invalid response for an origin could persist an attacker-chosen shared compression dictionary in the network service, undermining the guarantee that dictionaries derive only from validly authenticated connections. Any resulting cross-origin information disclosure or response-interpretation manipulation from reusing the poisoned dictionary is (inferred), as the patch demonstrates only the improper persistence, not a concrete downstream primitive. The vendor advisory rates this low severity and does not report in-the-wild exploitation; NVD scores it CVSS 4.3.

Changed Functions

FunctionChangeNotes
if
services/network/cors/cors_url_loader.cc
modified
TEST_F
services/network/cors/cors_url_loader_shared_dictionary_unittest.cc
modified

Files Changed

  • services/network/cors/cors_url_loader.cc
  • services/network/cors/cors_url_loader_shared_dictionary_unittest.cc

Audit Directions

  • Persist-decisions that ignore `cert_status`
    Audit every site where response content or metadata is written to durable storage (caches, dictionaries, prefetch, HSTS-like state) to confirm the write path consults net::IsCertStatusError(response_head->cert_status) and not just header presence.
  • Header-presence-only guards
    Look for if (header) conditions that trigger storage or trust decisions without a parallel security/validity check on the connection that delivered the header.
  • Shared-dictionary lifecycle
    Review the rest of the compression-dictionary path (SharedDictionaryStorage::MaybeCreateWriter, dictionary selection/read for later requests, and non-CORS loaders) for analogous places where an invalid certificate, revoked cert, or other connection error should but may not veto persistence or reuse.
From e08e3ee28570e634bfa6e3a752fcaf9d8aa9f0d7 Mon Sep 17 00:00:00 2001
From: Patrick Meenan <[email protected]>
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 <[email protected]>
Reviewed-by: Tsuyoshi Horo <[email protected]>
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();
 
Loading diff…

Regression Test / PoC

shipped with the fix
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();
Loading diff…

Original Bug Report

reported by [email protected]

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.cc
  • services/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:

  1. On an attacker-controlled network, intercept requests to https://victim.example with a self-signed certificate.
  2. In Chrome, navigate to https://victim.example and click “Proceed” on the SSL interstitial warning.
  3. Serve a 200 OK response with the header Use-As-Dictionary: match="/*", id="MITM-PLANTED-12345", accompanied by a valid dictionary body and cache headers.
  4. Close the browser, move the client to a clean network, and restart Chrome.
  5. Navigate to the legitimate https://victim.example and observe that the outgoing request headers contain Dictionary-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.

View on issue tracker