CVE-2026-12025
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
TEST_Fservices/network/cors/cors_url_loader_unittest.cc |
modified | |
forservices/network/cors/cors_url_loader_unittest.cc |
modified |
Files Changed
services/network/cors/cors_url_loader_unittest.ccservices/network/public/cpp/cors/cors.ccservices/network/public/cpp/cors/cors.hservices/network/public/cpp/header_util.ccservices/network/public/cpp/header_util.hservices/network/public/cpp/header_util_unittest.cc
Patch
From 4a89dfe3d2f47eb18a92b4d934bcddfd8104e6de Mon Sep 17 00:00:00 2001 From: Kenichi Ishibashi <[email protected]> Date: Thu, 04 Jun 2026 04:54:19 -0700 Subject: [PATCH] Reject forbidden method override headers in CorsURLLoader Align Network Service header validation with the Fetch spec and Blink's client-side checks. Update network::IsRequestHeaderSafe() to reject X-HTTP-Method, X-HTTP-Method-Override, and X-Method-Override headers when their values contain forbidden methods (CONNECT, TRACE, or TRACK). Bug: 517153191 Change-Id: I785ca55bd3a87d78330794a73711734197490fd3 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7884830 Commit-Queue: Kenichi Ishibashi <[email protected]> Reviewed-by: Takashi Toyoshima <[email protected]> Cr-Commit-Position: refs/heads/main@{#1641573} --- diff --git a/services/network/cors/cors_url_loader_unittest.cc b/services/network/cors/cors_url_loader_unittest.cc index 2657b5e6..e007dfe5 100644 --- a/services/network/cors/cors_url_loader_unittest.cc +++ b/services/network/cors/cors_url_loader_unittest.cc @@ -165,6 +165,58 @@ } } +TEST_F(CorsURLLoaderTest, ForbiddenMethodOverride) { + const struct { + std::string header_name; + std::string header_value; + } kTestCases[] = { + {"X-HTTP-Method-Override", "TRACE"}, + {"X-HTTP-Method-Override", "TRACK"}, + {"X-HTTP-Method-Override", "CONNECT"}, + {"X-HTTP-Method", "TRACE"}, + {"X-Method-Override", "TRACE"}, + }; + for (const auto& test_case : kTestCases) { + SCOPED_TRACE(test_case.header_name); + SCOPED_TRACE(test_case.header_value); + for (const mojom::RequestMode mode : + {mojom::RequestMode::kSameOrigin, mojom::RequestMode::kNoCors, + mojom::RequestMode::kCors, + mojom::RequestMode::kCorsWithForcedPreflight, + mojom::RequestMode::kNavigate}) { + SCOPED_TRACE(mode); + + ResetFactory( + url::Origin::Create(GURL("https://example.com")) /* initiator */, + OriginatingProcessId::browser()); + + ResourceRequest request; + request.mode = mode; + request.credentials_mode = mojom::CredentialsMode::kInclude; + request.url = GURL("https://example.com/"); + request.request_initiator = url::Origin::Create(request.url); + request.method = "POST"; + request.headers.SetHeader(test_case.header_name, test_case.header_value); + + BadMessageTestHelper bad_message_helper; + CreateLoaderAndStart(request); + if (IsNetworkLoaderStarted()) { + RunUntilCreateLoaderAndStartCalled(); + NotifyLoaderClientOnReceiveResponse(); + NotifyLoaderClientOnComplete(net::OK); + } + RunUntilComplete(); + + EXPECT_FALSE(IsNetworkLoaderStarted()); + EXPECT_FALSE(client().has_received_redirect()); + EXPECT_FALSE(client().has_received_response()); + EXPECT_TRUE(client().has_received_completion()); + EXPECT_THAT(client().completion_status().error_code, + net::test::IsError(net::ERR_INVALID_ARGUMENT)); + } + } +} + TEST_F(CorsURLLoaderTest, SameOriginWithoutInitiator) { ResourceRequest request; request.mode = mojom::RequestMode::kSameOrigin; diff --git a/services/network/public/cpp/cors/cors.cc b/services/network/public/cpp/cors/cors.cc index 9347214..14334f7 100644 --- a/services/network/public/cpp/cors/cors.cc +++ b/services/network/public/cpp/cors/cors.cc @@ -463,7 +463,7 @@ return {"range"}; } -bool IsForbiddenMethod(const std::string& method) { +bool IsForbiddenMethod(std::string_view method) { const std::string upper_method = base::ToUpperASCII(method); return upper_method == net::HttpRequestHeaders::kConnectMethod || upper_method == net::HttpRequestHeaders::kTraceMethod || diff --git a/services/network/public/cpp/cors/cors.h b/services/network/public/cpp/cors/cors.h index cdafcb6d..8c086d5 100644 --- a/services/network/public/cpp/cors/cors.h +++ b/services/network/public/cpp/cors/cors.h @@ -123,7 +123,7 @@ // Checks forbidden method in the fetch spec. // See https://fetch.spec.whatwg.org/#forbidden-method. -COMPONENT_EXPORT(NETWORK_CPP) bool IsForbiddenMethod(const std::string& name); +COMPONENT_EXPORT(NETWORK_CPP) bool IsForbiddenMethod(std::string_view name); // Returns true if |type| is a response type which makes a response // CORS-same-origin. See https://html.spec.whatwg.org/C/#cors-same-origin. diff --git a/services/network/public/cpp/header_util.cc b/services/network/public/cpp/header_util.cc index a75f986..ae36313 100644 --- a/services/network/public/cpp/header_util.cc +++ b/services/network/public/cpp/header_util.cc @@ -17,6 +17,8 @@ #include "net/http/http_request_headers.h" #include "net/http/http_response_headers.h" #include "net/http/http_status_code.h" +#include "net/http/http_util.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" #include "url/gurl.h" @@ -95,6 +97,17 @@ if (base::StartsWith(key, "Proxy-", base::CompareCase::INSENSITIVE_ASCII)) return false; + if (base::EqualsCaseInsensitiveASCII(key, "X-HTTP-Method") || + base::EqualsCaseInsensitiveASCII(key, "X-HTTP-Method-Override") || + base::EqualsCaseInsensitiveASCII(key, "X-Method-Override")) { + net::HttpUtil::ValuesIterator method_iterator(value, ','); + while (method_iterator.GetNext()) { + if (cors::IsForbiddenMethod(method_iterator.value())) { + return false; + } + } + } + return true; } diff --git a/services/network/public/cpp/header_util.h b/services/network/public/cpp/header_util.h index 592528db..a24931a 100644 --- a/services/network/public/cpp/header_util.h +++ b/services/network/public/cpp/header_util.h @@ -22,6 +22,11 @@ } // namespace mojom // Checks if a single request header is safe to send. +// +// Per https://fetch.spec.whatwg.org/#forbidden-request-header, the method- +// override headers are forbidden when their value parses to a forbidden +// method. The logic is almost compatible but exclude some headers that would +// be set by renderer's internal code. COMPONENT_EXPORT(NETWORK_CPP) bool IsRequestHeaderSafe(std::string_view key, std::string_view value); diff --git a/services/network/public/cpp/header_util_unittest.cc b/services/network/public/cpp/header_util_unittest.cc index aa1649c..3795e8ad 100644 --- a/services/network/public/cpp/header_util_unittest.cc +++ b/services/network/public/cpp/header_util_unittest.cc @@ -39,6 +39,13 @@ {"Proxy-Foo", "bar", false}, {"PrOxY-FoO", "bar", false}, + {"X-HTTP-Method-Override", "TRACE", false}, + {"x-http-method-override", "trAcE", false}, + {"X-HTTP-Method-Override", "GET", true}, + {"X-HTTP-Method-Override", "GET, TRACE", false}, + {"X-HTTP-Method", "TRACK", false}, + {"X-Method-Override", "CONNECT", false}, + {"dnt", "1", true}, }; @@ -80,6 +87,13 @@ {"Proxy-Foo", "bar", false}, {"PrOxY-FoO", "bar", false}, + {"X-HTTP-Method-Override", "TRACE", false}, + {"x-http-method-override", "trAcE", false}, + {"X-HTTP-Method-Override", "GET", true}, + {"X-HTTP-Method-Override", "GET, TRACE", false}, + {"X-HTTP-Method", "TRACK", false}, + {"X-Method-Override", "CONNECT", false}, + {"dnt", "1", true}, };
Regression Test / PoC
diff --git a/services/network/cors/cors_url_loader_unittest.cc b/services/network/cors/cors_url_loader_unittest.cc
index 2657b5e6..e007dfe5 100644
--- a/services/network/cors/cors_url_loader_unittest.cc
+++ b/services/network/cors/cors_url_loader_unittest.cc
@@ -165,6 +165,58 @@
}
}
+TEST_F(CorsURLLoaderTest, ForbiddenMethodOverride) {
+ const struct {
+ std::string header_name;
+ std::string header_value;
+ } kTestCases[] = {
+ {"X-HTTP-Method-Override", "TRACE"},
+ {"X-HTTP-Method-Override", "TRACK"},
+ {"X-HTTP-Method-Override", "CONNECT"},
+ {"X-HTTP-Method", "TRACE"},
+ {"X-Method-Override", "TRACE"},
+ };
+ for (const auto& test_case : kTestCases) {
+ SCOPED_TRACE(test_case.header_name);
+ SCOPED_TRACE(test_case.header_value);
+ for (const mojom::RequestMode mode :
+ {mojom::RequestMode::kSameOrigin, mojom::RequestMode::kNoCors,
+ mojom::RequestMode::kCors,
+ mojom::RequestMode::kCorsWithForcedPreflight,
+ mojom::RequestMode::kNavigate}) {
+ SCOPED_TRACE(mode);
+
+ ResetFactory(
+ url::Origin::Create(GURL("https://example.com")) /* initiator */,
+ OriginatingProcessId::browser());
+
+ ResourceRequest request;
+ request.mode = mode;
+ request.credentials_mode = mojom::CredentialsMode::kInclude;
+ request.url = GURL("https://example.com/");
+ request.request_initiator = url::Origin::Create(request.url);
+ request.method = "POST";
+ request.headers.SetHeader(test_case.header_name, test_case.header_value);
+
+ BadMessageTestHelper bad_message_helper;
+ CreateLoaderAndStart(request);
+ if (IsNetworkLoaderStarted()) {
+ RunUntilCreateLoaderAndStartCalled();
+ NotifyLoaderClientOnReceiveResponse();
+ NotifyLoaderClientOnComplete(net::OK);
+ }
+ RunUntilComplete();
+
+ EXPECT_FALSE(IsNetworkLoaderStarted());
+ EXPECT_FALSE(client().has_received_redirect());
+ EXPECT_FALSE(client().has_received_response());
+ EXPECT_TRUE(client().has_received_completion());
+ EXPECT_THAT(client().completion_status().error_code,
+ net::test::IsError(net::ERR_INVALID_ARGUMENT));
+ }
+ }
+}
+
TEST_F(CorsURLLoaderTest, SameOriginWithoutInitiator) {
ResourceRequest request;
request.mode = mojom::RequestMode::kSameOrigin;
diff --git a/services/network/public/cpp/header_util_unittest.cc b/services/network/public/cpp/header_util_unittest.cc
index aa1649c..3795e8ad 100644
--- a/services/network/public/cpp/header_util_unittest.cc
+++ b/services/network/public/cpp/header_util_unittest.cc
@@ -39,6 +39,13 @@
{"Proxy-Foo", "bar", false},
{"PrOxY-FoO", "bar", false},
+ {"X-HTTP-Method-Override", "TRACE", false},
+ {"x-http-method-override", "trAcE", false},
+ {"X-HTTP-Method-Override", "GET", true},
+ {"X-HTTP-Method-Override", "GET, TRACE", false},
+ {"X-HTTP-Method", "TRACK", false},
+ {"X-Method-Override", "CONNECT", false},
+
{"dnt", "1", true},
};
@@ -80,6 +87,13 @@
{"Proxy-Foo", "bar", false},
{"PrOxY-FoO", "bar", false},
+ {"X-HTTP-Method-Override", "TRACE", false},
+ {"x-http-method-override", "trAcE", false},
+ {"X-HTTP-Method-Override", "GET", true},
+ {"X-HTTP-Method-Override", "GET, TRACE", false},
+ {"X-HTTP-Method", "TRACK", false},
+ {"X-Method-Override", "CONNECT", false},
+
{"dnt", "1", true},
};
Original Bug Report
Bypass of forbidden method restrictions using method-override headers in Network Service
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 potentially bypass forbidden method restrictions to exfiltrate cross-origin cookies. By sending a kNoCors request containing custom method-override headers (like X-HTTP-Method-Override), the request can bypass the Network Service trust boundary checks. If a target server honors these override headers, it can reflect cookies in a TRACE response that Opaque Response Blocking (ORB) fails open on.
Affected files:
services/network/public/cpp/header_util.ccservices/network/cors/cors_url_loader_factory.cc
Estimated timestamp from git blame: 2022-10-24
Potential Root Cause Analysis
The Fetch specification bans specific request headers (X-HTTP-Method-Override, X-HTTP-Method, and X-Method-Override) when their values parse to a forbidden method (CONNECT, TRACE, or TRACK).
Chrome enforces this restriction renderer-side in Blink via net::HttpUtil::IsSafeHeader (defined in net/http/http_util.cc at line 385), which explicitly parses custom override headers against the kForbiddenHeaderFieldsWithForbiddenMethod list and rejects them if they contain a forbidden method.
However, at the network-service trust boundary (services/network/cors/cors_url_loader_factory.cc), the validation relies on network::IsRequestHeaderSafe in services/network/public/cpp/header_util.cc (line 81) to verify request header safety. The kUnsafeHeaders blocklist in IsRequestHeaderSafe does not contain any of the custom method-override headers. Furthermore, the check for forbidden methods in CorsURLLoaderFactory::IsValidRequest (line 912) only inspects the main HTTP request method (request.method).
Because of this validation gap, a compromised renderer can directly invoke the Network Service Mojo interface to issue a request with a safe method (such as POST) containing an unsafe method override (such as X-HTTP-Method-Override: TRACE), bypassing both the forbidden method and CORS preflight checks.
Potential Attack Scenario / Trigger Path
Note: The steps described below are a theoretical attack scenario identified via static source code analysis. Our tooling does not currently have the capability to execute code or verify exploitation dynamically.
- Request Formulation: A compromised renderer sends a Mojo request to the Network Service via
URLLoaderFactory::CreateLoaderAndStartwith:method = 'POST'mode = mojom::RequestMode::kNoCorscredentials_mode = mojom::CredentialsMode::kIncludeheaders = {'X-HTTP-Method-Override': 'TRACE'}url = 'https://victim.com'(a cross-origin server configured to honor method override headers).
- Network Service Verification:
CorsURLLoaderFactory::IsValidRequestaccepts the request. The request headers are considered safe because ‘X-HTTP-Method-Override’ is not inkUnsafeHeaders. The forbidden method check passes becauseIsForbiddenMethod('POST')isfalse. No CORS preflight is triggered due tomode = kNoCors. - Request Transmission: The Network Service attaches the victim’s session cookies and transmits the request unaltered over the network.
- Server Dispatch: The victim server processes the request. Recognizing the
X-HTTP-Method-Override: TRACEheader, it handles the request as aTRACErequest and echoes back the original request headers (including the sensitive sessionCookieheader) in the response body. - ORB Evaluation: The response is returned, typically with a
message/httportext/plainMIME type. Opaque Response Blocking (ORB) evaluates the response headers:GetCanonicalMimeType('message/http')maps the MIME type toMimeType::kOthers(services/network/orb/orb_mimetypes.ccat line 223).- Since
kOthersis not on the blocklist or unconditionally blocked, ORB falls back to content sniffing. - Sniffing the reflected TRACE request headers does not match HTML, XML, or JSON signatures. Thus, ORB fails open and returns
Decision::kAllow(services/network/orb/orb_impl.ccat line 482).
- Exfiltration: The response body data pipe is forwarded to the compromised renderer. The renderer reads the reflected headers from the stream, exfiltrating the victim’s cross-origin session cookies.
Suggested Remediation
To resolve this issue, the validation within the Network Service must be aligned with the renderer-side security model.
Update network::IsRequestHeaderSafe (or add a check in AreRequestHeadersSafe / IsValidRequest) to validate that X-HTTP-Method-Override, X-HTTP-Method, and X-Method-Override headers do not contain forbidden methods (TRACE, TRACK, CONNECT), leveraging the same parsing logic present in net::HttpUtil::IsSafeHeader.
Evaluated with Chrome root at commit: b1520ef4a76878853a31f0943b565e42060edec8
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.