CVE-2026-79122
Overview
Background
- `UrlFetchRequestBase`
- The base class in
google_apisthat builds outgoing Google API requests, attaches anAuthorizationheader carrying the user’s OAuth access token, and drives them viaSimpleURLLoader. - `SimpleURLLoader`
- A network-service client helper that, by default, forwards explicitly configured request headers (including
Authorization) across HTTP redirects unless a redirect callback removes them. - Redirect callback (`SetOnRedirectCallback`)
- A hook invoked on each 3xx redirect that receives the pre- and post-redirect URLs and a mutable
to_be_removed_headerslist, letting the caller strip headers before the request is re-sent to the new location. - Same-origin check (`url::IsSameOriginWith`)
- Compares scheme/host/port of two URLs; a mismatch means the redirect target is a different origin and should not receive credentials scoped to the original origin.
Root Cause Analysis
UrlFetchRequestBase set the Authorization header as an explicitly configured header and executed the request through SimpleURLLoader, which forwards such headers across redirects by default and had no redirect callback registered. Consequently, when a Google API endpoint returned a 3xx redirect to a different origin, SimpleURLLoader re-sent the request to that origin with the OAuth access token still attached, leaking the credential to a third-party destination. The invariant violated is that an origin-scoped bearer token must never be transmitted to a different origin.
The fix registers UrlFetchRequestBase::OnRedirect via SetOnRedirectCallback, which compares url_before_redirect with redirect_info.new_url using url::IsSameOriginWith and, on a cross-origin redirect, appends net::HttpRequestHeaders::kAuthorization to to_be_removed_headers; same-origin redirects leave the header intact to preserve existing workflows. The regression tests confirm the header reaches the primary origin’s /redirect and /target but is absent on the cross-origin server, while a same-origin redirect retains it on both hops.
SimpleURLLoader’s default header-forwarding across redirects without a redirect callback, so an origin-scoped Authorization token followed cross-origin 3xx redirects; the fix adds an OnRedirect handler that strips the header whenever the redirect target is not same-origin.Attack Path
- Attacker controls or influences a redirecting endpoint
An API request built by
UrlFetchRequestBasetargets a URL that responds with a 3xx redirect whoseLocationpoints to an attacker-controlled cross-origin destination. (inferred) - Redirect forwarded with credentials
Pre-patch, with no redirect callback registered,
SimpleURLLoaderre-issues the request to the new origin and forwards the explicitly configuredAuthorizationheader unchanged. - Token delivered cross-origin
The attacker’s server receives the request bearing the victim’s OAuth access token in the
Authorizationheader, capturing the credential. - Credential misuse With the leaked access token the attacker can impersonate the user against Google APIs within the token’s scope. (inferred)
Impact Assessment
google_apis in the browser process. The vendor advisory rates this medium severity and does not report in-the-wild exploitation; NVD scores it CVSS 5.9. (inferred) The precondition is a cross-origin 3xx redirect on a request that carries the Authorization header.Changed Functions
| Function | Change | Notes |
|---|---|---|
Valuegoogle_apis/common/base_requests.h |
modified | |
RequestSendergoogle_apis/common/base_requests.h |
modified | |
TEST_Fgoogle_apis/common/base_requests_unittest.cc |
modified | |
TESTgoogle_apis/common/base_requests_unittest.cc |
modified |
Files Changed
google_apis/common/base_requests.ccgoogle_apis/common/base_requests_unittest.ccgoogle_apis/common/base_requests.h
Audit Directions
- `SimpleURLLoader` users attaching credentialsAudit all callers that set
Authorizationor other secret headers on aSimpleURLLoader/ResourceRequestand verify each registers a redirect callback that drops them on cross-origin (or all) redirects. - Default cross-redirect header forwardingSearch for network requests that add sensitive headers without
SetOnRedirectCallback, since the default behavior forwards explicitly configured headers to redirect targets. - Same-origin vs same-site scopingReview credential-stripping predicates to confirm they use
IsSameOriginWith(scheme+host+port) rather than a looser same-site or host-only comparison that could still leak to a different port/scheme.
Patch
From 49bb74f7f1589143f800972535ff296ad9dae6b9 Mon Sep 17 00:00:00 2001 From: Ming-Ying Chung <[email protected]> Date: Fri, 31 Jul 2026 00:28:10 -0700 Subject: [PATCH] [google_apis] Drop Authorization header on cross-origin redirects `UrlFetchRequestBase` attaches an `Authorization` header to outgoing Google API requests and executes them through `SimpleURLLoader`. `SimpleURLLoader` forwards explicitly configured headers across HTTP redirects. Consequently, if an API endpoint responds with a 3xx redirect to a different origin, the access token is forwarded to the redirected destination. To prevent credential leakage, register a redirect callback on `SimpleURLLoader` that checks if the redirect target is same-origin with the previous URL. Cross-origin redirects append the `Authorization` header to the list of headers to remove, while same-origin redirects retain the header for existing workflows. Bug: 513608831 Change-Id: I6b19f963c9ff3bb0b996b1fe29bbbe84d962d18e Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8050627 Commit-Queue: Ming-Ying Chung <[email protected]> Reviewed-by: Eriko Kurimoto <[email protected]> Cr-Commit-Position: refs/heads/main@{#1671672} --- diff --git a/google_apis/common/base_requests.cc b/google_apis/common/base_requests.cc index 037901c..6268c47 100644 --- a/google_apis/common/base_requests.cc +++ b/google_apis/common/base_requests.cc @@ -30,8 +30,10 @@ #include "net/http/http_request_headers.h" #include "net/http/http_response_headers.h" #include "net/http/http_util.h" +#include "net/url_request/redirect_info.h" #include "services/network/public/cpp/resource_request.h" #include "services/network/public/mojom/url_response_head.mojom.h" +#include "url/origin.h" #if BUILDFLAG(IS_POSIX) #include <fcntl.h> @@ -308,9 +310,24 @@ url_loader_->SetOnResponseStartedCallback(base::BindOnce( &UrlFetchRequestBase::OnResponseStarted, weak_ptr_factory_.GetWeakPtr())); + url_loader_->SetOnRedirectCallback(base::BindRepeating( + &UrlFetchRequestBase::OnRedirect, weak_ptr_factory_.GetWeakPtr())); + url_loader_->DownloadAsStream(sender_->url_loader_factory(), this); } +void UrlFetchRequestBase::OnRedirect( + const GURL& url_before_redirect, + const net::RedirectInfo& redirect_info, + const network::mojom::URLResponseHead& response_head, + std::vector<std::string>* to_be_removed_headers) { + // Strip the Authorization header on cross-origin redirects to prevent + // sensitive access tokens from leaking to third-party destinations. + if (!url::IsSameOriginWith(url_before_redirect, redirect_info.new_url)) { + to_be_removed_headers->push_back(net::HttpRequestHeaders::kAuthorization); + } +} + void UrlFetchRequestBase::OnDownloadProgress(ProgressCallback progress_callback, uint64_t current) { progress_callback.Run(static_cast<int64_t>(current), diff --git a/google_apis/common/base_requests.h b/google_apis/common/base_requests.h index 7e0dda0..605a72a 100644 --- a/google_apis/common/base_requests.h +++ b/google_apis/common/base_requests.h @@ -32,6 +32,10 @@ class Value; } // namespace base +namespace net { +struct RedirectInfo; +} // namespace net + namespace google_apis { class RequestSender; @@ -258,6 +262,12 @@ void OnResponseStarted(const GURL& final_url, const network::mojom::URLResponseHead& response_head); + // Called when SimpleURLLoader encounters a redirect. + void OnRedirect(const GURL& url_before_redirect, + const net::RedirectInfo& redirect_info, + const network::mojom::URLResponseHead& response_head, + std::vector<std::string>* to_be_removed_headers); + // Invokes callback with |code| and request to delete the request to // |sender_|. void CompleteRequestWithError(ApiErrorCode code); diff --git a/google_apis/common/base_requests_unittest.cc b/google_apis/common/base_requests_unittest.cc index 3892cff4..7fc435d40 100644 --- a/google_apis/common/base_requests_unittest.cc +++ b/google_apis/common/base_requests_unittest.cc @@ -18,7 +18,9 @@ #include "google_apis/common/test_util.h" #include "mojo/public/cpp/bindings/pending_remote.h" #include "mojo/public/cpp/bindings/remote.h" +#include "net/http/http_request_headers.h" #include "net/test/embedded_test_server/embedded_test_server.h" +#include "net/test/embedded_test_server/http_request.h" #include "net/test/embedded_test_server/http_response.h" #include "net/traffic_annotation/network_traffic_annotation_test_helper.h" #include "services/network/network_service.h" @@ -129,16 +131,36 @@ task_environment_.RunUntilIdle(); } + // Handles incoming HTTP requests on the primary test server. Records each + // request to verify header delivery and issues a 302 redirect for the + // `/redirect` path if a target URL is configured. std::unique_ptr<net::test_server::HttpResponse> HandleRequest( const net::test_server::HttpRequest& request) { - std::unique_ptr<net::test_server::BasicHttpResponse> response( - new net::test_server::BasicHttpResponse); + received_requests_.push_back(request); + auto response = std::make_unique<net::test_server::BasicHttpResponse>(); + if (redirect_location_.is_valid() && request.relative_url == "/redirect") { + response->set_code(net::HTTP_FOUND); + response->AddCustomHeader("Location", redirect_location_.spec()); + return std::move(response); + } response->set_code(response_code_); response->set_content(response_body_); response->set_content_type("application/json"); return std::move(response); } + // Handles incoming HTTP requests on the secondary cross-origin server. + // Records received requests to inspect headers delivered across origin + // boundaries. + std::unique_ptr<net::test_server::HttpResponse> HandleOtherOriginRequest( + const net::test_server::HttpRequest& request) { + other_origin_requests_.push_back(request); + auto response = std::make_unique<net::test_server::BasicHttpResponse>(); + response->set_code(net::HTTP_OK); + response->set_content_type("application/json"); + return std::move(response); + } + base::test::TaskEnvironment task_environment_{ base::test::TaskEnvironment::MainThreadType::IO}; std::unique_ptr<network::mojom::NetworkService> network_service_; @@ -148,9 +170,13 @@ test_shared_loader_factory_; std::unique_ptr<RequestSender> sender_; net::EmbeddedTestServer test_server_; + net::EmbeddedTestServer other_origin_server_; net::HttpStatusCode response_code_; std::string response_body_; + GURL redirect_location_; + std::vector<net::test_server::HttpRequest> received_requests_; + std::vector<net::test_server::HttpRequest> other_origin_requests_; }; TEST_F(BaseRequestsTest, ParseValidJson) { @@ -198,6 +224,60 @@ EXPECT_EQ(HTTP_SERVICE_UNAVAILABLE, error); } +// Verifies that the Authorization header is removed when a request is +// redirected to a cross-origin destination. +TEST_F(BaseRequestsTest, AuthorizationHeaderRemovedOnCrossOriginRedirect) { + other_origin_server_.RegisterRequestHandler(base::BindRepeating( + &BaseRequestsTest::HandleOtherOriginRequest, base::Unretained(this))); + ASSERT_TRUE(other_origin_server_.Start()); + ASSERT_NE(test_server_.base_url(), other_origin_server_.base_url()); + + redirect_location_ = other_origin_server_.GetURL("/target"); + + ApiErrorCode error = OTHER_ERROR; + base::RunLoop run_loop; + sender_->StartRequestWithAuthRetry(std::make_unique<FakeUrlFetchRequest>( + sender_.get(), + test_util::CreateQuitCallback( + &run_loop, test_util::CreateCopyResultCallback(&error)), + test_server_.GetURL("/redirect"))); + run_loop.Run(); + + EXPECT_EQ(HTTP_SUCCESS, error); + ASSERT_EQ(1u, received_requests_.size()); + EXPECT_NE(received_requests_[0].headers.end(), + received_requests_[0].headers.find( + net::HttpRequestHeaders::kAuthorization)); + ASSERT_EQ(1u, other_origin_requests_.size()); + EXPECT_EQ(other_origin_requests_[0].headers.end(), + other_origin_requests_[0].headers.find( + net::HttpRequestHeaders::kAuthorization)); +} + +// Verifies that the Authorization header is preserved when a request is +// redirected to a same-origin destination. +TEST_F(BaseRequestsTest, AuthorizationHeaderKeptOnSameOriginRedirect) { + redirect_location_ = test_server_.GetURL("/target"); + + ApiErrorCode error = OTHER_ERROR; + base::RunLoop run_loop; + sender_->StartRequestWithAuthRetry(std::make_unique<FakeUrlFetchRequest>( + sender_.get(), + test_util::CreateQuitCallback( + &run_loop, test_util::CreateCopyResultCallback(&error)), + test_server_.GetURL("/redirect"))); + run_loop.Run(); + + EXPECT_EQ(HTTP_SUCCESS, error); + ASSERT_EQ(2u, received_requests_.size()); + EXPECT_NE(received_requests_[0].headers.end(), + received_requests_[0].headers.find( + net::HttpRequestHeaders::kAuthorization)); + EXPECT_NE(received_requests_[1].headers.end(), + received_requests_[1].headers.find( + net::HttpRequestHeaders::kAuthorization)); +} + TEST(BaseRequestsHttpRequestMethodEnumTest, ConvertsToString) { EXPECT_EQ(HttpRequestMethodToString(HttpRequestMethod::kGet), "GET"); EXPECT_EQ(HttpRequestMethodToString(HttpRequestMethod::kPost), "POST");
Regression Test / PoC
diff --git a/google_apis/common/base_requests_unittest.cc b/google_apis/common/base_requests_unittest.cc
index 3892cff4..7fc435d40 100644
--- a/google_apis/common/base_requests_unittest.cc
+++ b/google_apis/common/base_requests_unittest.cc
@@ -18,7 +18,9 @@
#include "google_apis/common/test_util.h"
#include "mojo/public/cpp/bindings/pending_remote.h"
#include "mojo/public/cpp/bindings/remote.h"
+#include "net/http/http_request_headers.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
+#include "net/test/embedded_test_server/http_request.h"
#include "net/test/embedded_test_server/http_response.h"
#include "net/traffic_annotation/network_traffic_annotation_test_helper.h"
#include "services/network/network_service.h"
@@ -129,16 +131,36 @@
task_environment_.RunUntilIdle();
}
+ // Handles incoming HTTP requests on the primary test server. Records each
+ // request to verify header delivery and issues a 302 redirect for the
+ // `/redirect` path if a target URL is configured.
std::unique_ptr<net::test_server::HttpResponse> HandleRequest(
const net::test_server::HttpRequest& request) {
- std::unique_ptr<net::test_server::BasicHttpResponse> response(
- new net::test_server::BasicHttpResponse);
+ received_requests_.push_back(request);
+ auto response = std::make_unique<net::test_server::BasicHttpResponse>();
+ if (redirect_location_.is_valid() && request.relative_url == "/redirect") {
+ response->set_code(net::HTTP_FOUND);
+ response->AddCustomHeader("Location", redirect_location_.spec());
+ return std::move(response);
+ }
response->set_code(response_code_);
response->set_content(response_body_);
response->set_content_type("application/json");
return std::move(response);
}
+ // Handles incoming HTTP requests on the secondary cross-origin server.
+ // Records received requests to inspect headers delivered across origin
+ // boundaries.
+ std::unique_ptr<net::test_server::HttpResponse> HandleOtherOriginRequest(
+ const net::test_server::HttpRequest& request) {
+ other_origin_requests_.push_back(request);
+ auto response = std::make_unique<net::test_server::BasicHttpResponse>();
+ response->set_code(net::HTTP_OK);
+ response->set_content_type("application/json");
+ return std::move(response);
+ }
+
base::test::TaskEnvironment task_environment_{
base::test::TaskEnvironment::MainThreadType::IO};
std::unique_ptr<network::mojom::NetworkService> network_service_;
@@ -148,9 +170,13 @@
test_shared_loader_factory_;
std::unique_ptr<RequestSender> sender_;
net::EmbeddedTestServer test_server_;
+ net::EmbeddedTestServer other_origin_server_;
net::HttpStatusCode response_code_;
std::string response_body_;
+ GURL redirect_location_;
+ std::vector<net::test_server::HttpRequest> received_requests_;
+ std::vector<net::test_server::HttpRequest> other_origin_requests_;
};
TEST_F(BaseRequestsTest, ParseValidJson) {
@@ -198,6 +224,60 @@
EXPECT_EQ(HTTP_SERVICE_UNAVAILABLE, error);
}
+// Verifies that the Authorization header is removed when a request is
+// redirected to a cross-origin destination.
+TEST_F(BaseRequestsTest, AuthorizationHeaderRemovedOnCrossOriginRedirect) {
+ other_origin_server_.RegisterRequestHandler(base::BindRepeating(
+ &BaseRequestsTest::HandleOtherOriginRequest, base::Unretained(this)));
+ ASSERT_TRUE(other_origin_server_.Start());
+ ASSERT_NE(test_server_.base_url(), other_origin_server_.base_url());
+
+ redirect_location_ = other_origin_server_.GetURL("/target");
+
+ ApiErrorCode error = OTHER_ERROR;
+ base::RunLoop run_loop;
+ sender_->StartRequestWithAuthRetry(std::make_unique<FakeUrlFetchRequest>(
+ sender_.get(),
+ test_util::CreateQuitCallback(
+ &run_loop, test_util::CreateCopyResultCallback(&error)),
+ test_server_.GetURL("/redirect")));
+ run_loop.Run();
+
+ EXPECT_EQ(HTTP_SUCCESS, error);
+ ASSERT_EQ(1u, received_requests_.size());
+ EXPECT_NE(received_requests_[0].headers.end(),
+ received_requests_[0].headers.find(
+ net::HttpRequestHeaders::kAuthorization));
+ ASSERT_EQ(1u, other_origin_requests_.size());
+ EXPECT_EQ(other_origin_requests_[0].headers.end(),
+ other_origin_requests_[0].headers.find(
+ net::HttpRequestHeaders::kAuthorization));
+}
+
+// Verifies that the Authorization header is preserved when a request is
+// redirected to a same-origin destination.
+TEST_F(BaseRequestsTest, AuthorizationHeaderKeptOnSameOriginRedirect) {
+ redirect_location_ = test_server_.GetURL("/target");
+
+ ApiErrorCode error = OTHER_ERROR;
+ base::RunLoop run_loop;
+ sender_->StartRequestWithAuthRetry(std::make_unique<FakeUrlFetchRequest>(
+ sender_.get(),
+ test_util::CreateQuitCallback(
+ &run_loop, test_util::CreateCopyResultCallback(&error)),
+ test_server_.GetURL("/redirect")));
+ run_loop.Run();
+
+ EXPECT_EQ(HTTP_SUCCESS, error);
+ ASSERT_EQ(2u, received_requests_.size());
+ EXPECT_NE(received_requests_[0].headers.end(),
+ received_requests_[0].headers.find(
+ net::HttpRequestHeaders::kAuthorization));
+ EXPECT_NE(received_requests_[1].headers.end(),
+ received_requests_[1].headers.find(
+ net::HttpRequestHeaders::kAuthorization));
+}
+
TEST(BaseRequestsHttpRequestMethodEnumTest, ConvertsToString) {
EXPECT_EQ(HttpRequestMethodToString(HttpRequestMethod::kGet), "GET");
EXPECT_EQ(HttpRequestMethodToString(HttpRequestMethod::kPost), "POST");
Original Bug Report
Potential OAuth token leakage via cross-origin redirects in google_apis::UrlFetchRequestBase
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 UrlFetchRequestBase class, used by numerous browser-process Google API clients, fails to strip the sensitive Authorization header during cross-origin redirects. This behavior potentially discloses OAuth Bearer tokens to unauthorized third-party origins, violating Step 13 of the Fetch specification. Impacted services include Google Drive, Calendar, and YouTube Music, which may leak tokens through HTTP redirects or malicious server-supplied URLs.
Affected files:
google_apis/common/base_requests.ccgoogle_apis/drive/drive_base_requests.ccgoogle_apis/drive/drive_api_requests.cc
Estimated timestamp from git blame: Unknown (Google3 checkout)
Background
In Chromium, the google_apis::UrlFetchRequestBase class is the primary base class for browser-process HTTP requests to Google APIs. It manages authentication by attaching an Authorization: Bearer [token] header to requests dispatched via network::SimpleURLLoader.
Potential Root Cause
The vulnerability stems from the fact that UrlFetchRequestBase manually adds the Authorization header but does not register a redirect callback to ensure its removal during cross-origin transitions.
- Manual Header Attachment: In
google_apis/common/base_requests.cc,UrlFetchRequestBase::StartAfterPrepareadds the header:request->headers.AddHeaderFromString(base::StringPrintf("Authorization: Bearer %s", access_token.data()));. - Redirect Handling Omission: The class initializes
SimpleURLLoaderwithout callingSetOnRedirectCallback. When a redirect occurs,SimpleURLLoaderfollows it by default. - Network Stack Behavior: In Chromium’s network stack, the
net::RedirectUtil::UpdateHttpRequestutility (used bynet::URLRequest) only strips headers explicitly provided in aremoved_headerslist. While Blink’s renderer-sideResourceLoadercorrectly populates this list withAuthorizationfor cross-origin redirects, the browser-process implementation inUrlFetchRequestBasedoes not, leading to the header being preserved across origins.
Potential Exploitation Scenarios
An attacker could potentially receive a user’s OAuth token through two primary vectors:
- Cross-Origin HTTP Redirects: If a targeted Google API endpoint (e.g.,
www.googleapis.com) has an open-redirect vulnerability or is manipulated via a network-level attack, an HTTP 301/302 response can redirect the authenticated request tohttps://attacker.test/, which would receive the Bearer token in the request headers. - Application-Level URL Injection: Some services, such as Google Drive, parse URLs from JSON responses (e.g., the
nextLinkfield for pagination) orLocationheaders (for resumable upload session URLs). Analysis suggests these URLs are used blindly to initiate new authenticated requests. An attacker who can influence these fields (e.g., through a compromised network process or a specific API state) can direct a new request—complete with theAuthorizationheader—to an arbitrary third-party origin.
Impact
This issue potentially affects every component using google_apis::RequestSender, including:
- Google Drive: Disclosure of
drivescope tokens. - Birch/Calendar: Disclosure of
calendar.events.readonlytokens. - YouTube Music: Disclosure of focus-mode related tokens.
- Glanceables (Tasks/Classroom): Disclosure of various productivity scopes.
Suggested Potential Steps to Trigger
Note: These steps are based on code analysis and have not been verified with a functional Proof of Concept.
- Sign in to a Chrome profile to ensure authenticated services are active.
- Trigger a background request from an affected service (e.g., a Drive sync or Calendar poll).
- Use a proxy or simulated network environment to return an HTTP 302 redirect from the Google API endpoint to an external domain (e.g.,
https://attacker.example/). - Monitor the outgoing request to the external domain to verify if the
Authorization: Bearerheader is present.
Suggested Fix
- In
UrlFetchRequestBase, implement a redirect callback usingSimpleURLLoader::SetOnRedirectCallbackthat explicitly addsnet::HttpRequestHeaders::kAuthorizationto theremoved_headerslist if the redirect is cross-origin. - Update Google API consumers (particularly in
google_apis/drive/) to validate that any server-supplied URL (such asnextLinkor upload session URLs) matches an expected Google origin before using it in aRequestSenderflow.
Evaluated with Chrome root at commit: 1a8d40fc44df2088d5945c0bf53584038aa1614a
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.