CVE-2026-17660
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
BindRepeatingcontent/browser/browser_main_loop.cc |
modified | |
forcontent/browser/network_context_client_base_impl.cc |
modified | |
ifcontent/browser/network_context_client_base_impl.cc |
modified | |
TEST_Fcontent/browser/network_context_client_base_impl_unittest.cc |
modified |
Files Changed
content/browser/BUILD.gncontent/browser/browser_main_loop.cccontent/browser/loader/navigation_url_loader_impl.cccontent/browser/loader/navigation_url_loader_impl.hcontent/browser/network_context_client_base_impl.cccontent/browser/network_context_client_base_impl_unittest.cc
Patch
From 1fc7b5f1b300973956c4edcb7e3f8f8d56a56e6a Mon Sep 17 00:00:00 2001
From: Nidhi Jaju <nidhijaju@chromium.org>
Date: Mon, 06 Jul 2026 03:52:17 -0700
Subject: [PATCH] Validate browser-initiated file uploads
This CL introduces token-based tracking and validation for files
uploaded by the browser process to the network service.
We introduce `BrowserFileAccessCallbacks` to `SimpleURLLoader`, allowing
the browser to register file paths with `ChildProcessSecurityPolicy`.
`ScopedBrowserFileAccess` manages token lifetimes for file paths
originating from navigations (e.g., Web Share Target).
Finally, `NetworkContextClientBaseImpl::HandleFileUploadRequest` is
updated to validate the tokens using `CanBrowserReadFile()` when the
originating process is the browser. This entire mechanism is gated
behind the `kFileUploadTokenRegistration` feature flag, which is
enabled by default.
Bug: 497428001
Change-Id: I8a06849dc69b19d3fa71c62ba2ed63813db686fa
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7968017
Reviewed-by: Alex Moshchuk <alexmos@chromium.org>
Auto-Submit: Nidhi Jaju <nidhijaju@chromium.org>
Commit-Queue: Nidhi Jaju <nidhijaju@chromium.org>
Reviewed-by: Takashi Toyoshima <toyoshim@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1657089}
---
diff --git a/content/browser/BUILD.gn b/content/browser/BUILD.gn
index 6be9945..700ebede 100644
--- a/content/browser/BUILD.gn
+++ b/content/browser/BUILD.gn
@@ -2172,6 +2172,8 @@
"scheduler/responsiveness/watcher.h",
"scoped_active_url.cc",
"scoped_active_url.h",
+ "scoped_browser_file_access.cc",
+ "scoped_browser_file_access.h",
"screen_details/screen_change_monitor.cc",
"screen_details/screen_change_monitor.h",
"screen_orientation/screen_orientation_provider.cc",
diff --git a/content/browser/browser_main_loop.cc b/content/browser/browser_main_loop.cc
index 865c302..bc44a5da 100644
--- a/content/browser/browser_main_loop.cc
+++ b/content/browser/browser_main_loop.cc
@@ -112,6 +112,7 @@
#include "content/public/browser/browser_main_parts.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
+#include "content/public/browser/child_process_security_policy.h"
#include "content/public/browser/content_browser_client.h"
#include "content/public/browser/device_service.h"
#include "content/public/browser/network_service_instance.h"
@@ -147,7 +148,9 @@
#include "services/audio/service.h"
#include "services/data_decoder/public/cpp/service_provider.h"
#include "services/data_decoder/public/mojom/data_decoder_service.mojom.h"
+#include "services/network/public/cpp/features.h"
#include "services/network/public/cpp/network_switches.h"
+#include "services/network/public/cpp/simple_url_loader.h"
#include "services/network/public/mojom/network_service.mojom.h"
#include "services/network/transitional_url_loader_factory_owner.h"
#include "services/tracing/public/cpp/background_tracing/background_tracing_manager.h"
@@ -830,6 +833,24 @@
RenderProcessHost::SetRunRendererInProcess(true);
#endif
+ // Set up the callbacks used by the network layer to track and validate
+ // file access for browser-initiated uploads.
+ if (base::FeatureList::IsEnabled(
+ network::features::kBrowserInitiatedFileUploadValidation)) {
+ network::SimpleURLLoader::FileUploadEventCallbacks callbacks;
+ callbacks.register_callback = base::BindRepeating(
+ [](const base::UnguessableToken& token, const base::FilePath& path) {
+ ChildProcessSecurityPolicy::GetInstance()->GrantFileForBrowserUpload(
+ token, path);
+ });
+ callbacks.revoke_callback =
+ base::BindRepeating([](const base::UnguessableToken& token) {
+ ChildProcessSecurityPolicy::GetInstance()->RevokeFileForBrowserUpload(
+ token);
+ });
+ network::SimpleURLLoader::SetFileUploadEventCallbacks(callbacks);
+ }
+
// Initialize origins that require process isolation. Must be done
// after base::FeatureList is initialized, but before any navigations can
// happen.
diff --git a/content/browser/loader/navigation_url_loader_impl.cc b/content/browser/loader/navigation_url_loader_impl.cc
index d004578..c5073932 100644
--- a/content/browser/loader/navigation_url_loader_impl.cc
+++ b/content/browser/loader/navigation_url_loader_impl.cc
@@ -2170,6 +2170,17 @@
browser_context_, storage_partition_, frame_tree_node,
ukm::SourceIdObj::FromInt64(ukm_source_id_), &bypass_redirect_checks_,
allow_same_site_none_cookies_override_);
+
+ if (base::FeatureList::IsEnabled(
+ network::features::kBrowserInitiatedFileUploadValidation) &&
+ resource_request_->request_body) {
+ std::vector<base::FilePath> files =
+ resource_request_->request_body->GetReferencedFiles();
+ if (!files.empty()) {
+ scoped_browser_file_access_ =
+ std::make_unique<ScopedBrowserFileAccess>(std::move(files));
+ }
+ }
}
// static
diff --git a/content/browser/loader/navigation_url_loader_impl.h b/content/browser/loader/navigation_url_loader_impl.h
index 8dcbce48..9dbfdec 100644
--- a/content/browser/loader/navigation_url_loader_impl.h
+++ b/content/browser/loader/navigation_url_loader_impl.h
@@ -14,6 +14,7 @@
#include "content/browser/loader/navigation_url_loader.h"
#include "content/browser/loader/response_head_update_params.h"
#include "content/browser/navigation_subresource_loader_params.h"
+#include "content/browser/scoped_browser_file_access.h"
#include "content/common/content_export.h"
#include "content/public/browser/frame_tree_node_id.h"
#include "content/public/browser/global_request_id.h"
@@ -599,6 +600,12 @@
// response.
ResponseHeadUpdateParams head_update_params_;
+ // If the navigation request includes a file upload, this object ensures the
+ // browser process is aware that the Network Service is allowed to read the
+ // files. The files are granted access upon creation and revoked when this
+ // loader is destroyed.
+ std::unique_ptr<ScopedBrowserFileAccess> scoped_browser_file_access_;
+
base::WeakPtrFactory<NavigationURLLoaderImpl> weak_factory_{this};
};
diff --git a/content/browser/network_context_client_base_impl.cc b/content/browser/network_context_client_base_impl.cc
index ecc0497c..b4c5a2b4 100644
--- a/content/browser/network_context_client_base_impl.cc
+++ b/content/browser/network_context_client_base_impl.cc
@@ -19,6 +19,7 @@
#include "content/public/common/content_client.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "net/base/net_errors.h"
+#include "services/network/public/cpp/features.h"
#include "services/network/public/mojom/trust_tokens.mojom.h"
namespace content {
@@ -38,9 +39,17 @@
(async ? base::File::FLAG_ASYNC : 0);
ChildProcessSecurityPolicy* cpsp = ChildProcessSecurityPolicy::GetInstance();
for (const auto& file_path : file_paths) {
- if (!process_id.is_browser() &&
- !cpsp->CanReadFile(ToChildProcessId(process_id.renderer_process_id()),
- file_path)) {
+ bool access_denied = false;
+ if (base::FeatureList::IsEnabled(
+ network::features::kBrowserInitiatedFileUploadValidation) &&
+ process_id.is_browser()) {
+ access_denied = !cpsp->CanReadFileForBrowserUpload(file_path);
+ } else if (!process_id.is_browser()) {
+ access_denied = !cpsp->CanReadFile(
+ ToChildProcessId(process_id.renderer_process_id()), file_path);
+ }
+
+ if (access_denied) {
task_runner->PostTask(
FROM_HERE, base::BindOnce(std::move(callback), net::ERR_ACCESS_DENIED,
std::vector<base::File>()));
diff --git a/content/browser/network_context_client_base_impl_unittest.cc b/content/browser/network_context_client_base_impl_unittest.cc
index 2cbf0e6..7da23ceb 100644
--- a/content/browser/network_context_client_base_impl_unittest.cc
+++ b/content/browser/network_context_client_base_impl_unittest.cc
@@ -14,7 +14,9 @@
#include "base/strings/string_view_util.h"
#include "base/test/task_environment.h"
#include "base/test/test_file_util.h"
+#include "base/test/test_future.h"
#include "base/types/fixed_array.h"
+#include "base/unguessable_token.h"
#include "build/build_config.h"
#include "content/browser/security/cpsp/child_process_security_policy_impl.h"
#include "content/public/browser/network_context_client_base.h"
@@ -223,19 +225,39 @@
EXPECT_EQ(0U, response.opened_files.size());
}
-TEST_F(NetworkContextClientBaseTest, UploadFromBrowserProcess) {
+TEST_F(NetworkContextClientBaseTest,
+ OnFileUploadRequested_BrowserProcess_AccessDenied) {
base::FilePath path = temp_dir_.GetPath().AppendASCII("filename");
CreateFile(path, kFileContent1);
- // No grant necessary for browser process.
- UploadResponse response;
+ base::test::TestFuture<int, std::vector<base::File>> future;
client_.OnFileUploadRequested(
network::OriginatingProcessId::browser(), false, {path},
- /*destination_url=*/GURL(), std::move(response.callback));
- task_environment_.RunUntilIdle();
Regression Test / PoC
diff --git a/content/browser/network_context_client_base_impl_unittest.cc b/content/browser/network_context_client_base_impl_unittest.cc
index 2cbf0e6..7da23ceb 100644
--- a/content/browser/network_context_client_base_impl_unittest.cc
+++ b/content/browser/network_context_client_base_impl_unittest.cc
@@ -14,7 +14,9 @@
#include "base/strings/string_view_util.h"
#include "base/test/task_environment.h"
#include "base/test/test_file_util.h"
+#include "base/test/test_future.h"
#include "base/types/fixed_array.h"
+#include "base/unguessable_token.h"
#include "build/build_config.h"
#include "content/browser/security/cpsp/child_process_security_policy_impl.h"
#include "content/public/browser/network_context_client_base.h"
@@ -223,19 +225,39 @@
EXPECT_EQ(0U, response.opened_files.size());
}
-TEST_F(NetworkContextClientBaseTest, UploadFromBrowserProcess) {
+TEST_F(NetworkContextClientBaseTest,
+ OnFileUploadRequested_BrowserProcess_AccessDenied) {
base::FilePath path = temp_dir_.GetPath().AppendASCII("filename");
CreateFile(path, kFileContent1);
- // No grant necessary for browser process.
- UploadResponse response;
+ base::test::TestFuture<int, std::vector<base::File>> future;
client_.OnFileUploadRequested(
network::OriginatingProcessId::browser(), false, {path},
- /*destination_url=*/GURL(), std::move(response.callback));
- task_environment_.RunUntilIdle();
- EXPECT_EQ(net::OK, response.error_code);
- ASSERT_EQ(1U, response.opened_files.size());
- ValidateFileContents(response.opened_files[0], kFileContent1);
+ /*destination_url=*/GURL(), future.GetCallback());
+ EXPECT_EQ(net::ERR_ACCESS_DENIED, future.Get<0>());
+ EXPECT_EQ(0U, future.Get<1>().size());
+}
+
+TEST_F(NetworkContextClientBaseTest,
+ OnFileUploadRequested_BrowserProcess_AccessGranted) {
+ base::FilePath path = temp_dir_.GetPath().AppendASCII("filename");
+ CreateFile(path, kFileContent1);
+
+ base::UnguessableToken token = base::UnguessableToken::Create();
+ ChildProcessSecurityPolicyImpl::GetInstance()->GrantFileForBrowserUpload(
+ token, path);
+
+ base::test::TestFuture<int, std::vector<base::File>> future;
+ client_.OnFileUploadRequested(
+ network::OriginatingProcessId::browser(), false, {path},
+ /*destination_url=*/GURL(), future.GetCallback());
+ EXPECT_EQ(net::OK, future.Get<0>());
+ std::vector<base::File> opened_files = std::get<1>(future.Take());
+ ASSERT_EQ(1U, opened_files.size());
+ ValidateFileContents(opened_files[0], kFileContent1);
+
+ ChildProcessSecurityPolicyImpl::GetInstance()->RevokeFileForBrowserUpload(
+ token);
}
} // namespace content
diff --git a/content/browser/security/cpsp/child_process_security_policy_unittest.cc b/content/browser/security/cpsp/child_process_security_policy_unittest.cc
index 9f442fed..11cec88 100644
--- a/content/browser/security/cpsp/child_process_security_policy_unittest.cc
+++ b/content/browser/security/cpsp/child_process_security_policy_unittest.cc
@@ -3595,6 +3595,101 @@
p->EraseOriginAgentClusterState(browsing_instance_id);
}
+TEST_P(ChildProcessSecurityPolicyTest,
+ BrowserFileAccess_GrantAndRevokeSingleToken) {
+ auto* p = ChildProcessSecurityPolicyImpl::GetInstance();
+ base::FilePath file(TEST_PATH("/foo/bar"));
+ base::UnguessableToken token = base::UnguessableToken::Create();
+
+ EXPECT_FALSE(p->CanReadFileForBrowserUpload(file));
+
+ p->GrantFileForBrowserUpload(token, file);
+ EXPECT_TRUE(p->CanReadFileForBrowserUpload(file));
+
+ p->RevokeFileForBrowserUpload(token);
+ EXPECT_FALSE(p->CanReadFileForBrowserUpload(file));
+}
+
+TEST_P(ChildProcessSecurityPolicyTest,
+ BrowserFileAccess_MultipleTokensSameFile) {
+ auto* p = ChildProcessSecurityPolicyImpl::GetInstance();
+ base::FilePath file(TEST_PATH("/foo/bar"));
+ base::UnguessableToken token1 = base::UnguessableToken::Create();
+ base::UnguessableToken token2 = base::UnguessableToken::Create();
+
+ EXPECT_FALSE(p->CanReadFileForBrowserUpload(file));
+
+ p->GrantFileForBrowserUpload(token1, file);
+ p->GrantFileForBrowserUpload(token2, file);
+ EXPECT_TRUE(p->CanReadFileForBrowserUpload(file));
+
+ p->RevokeFileForBrowserUpload(token1);
+ EXPECT_TRUE(p->CanReadFileForBrowserUpload(file));
+
+ p->RevokeFileForBrowserUpload(token2);
+ EXPECT_FALSE(p->CanReadFileForBrowserUpload(file));
+}
+
+TEST_P(ChildProcessSecurityPolicyTest,
+ BrowserFileAccess_MultipleTokensDifferentFiles) {
+ auto* p = ChildProcessSecurityPolicyImpl::GetInstance();
+ base::FilePath file1(TEST_PATH("/foo/bar1"));
+ base::FilePath file2(TEST_PATH("/foo/bar2"));
+ base::UnguessableToken token1 = base::UnguessableToken::Create();
+ base::UnguessableToken token2 = base::UnguessableToken::Create();
+
+ p->GrantFileForBrowserUpload(token1, file1);
+ p->GrantFileForBrowserUpload(token2, file2);
+
+ EXPECT_TRUE(p->CanReadFileForBrowserUpload(file1));
+ EXPECT_TRUE(p->CanReadFileForBrowserUpload(file2));
+
+ p->RevokeFileForBrowserUpload(token1);
+ EXPECT_FALSE(p->CanReadFileForBrowserUpload(file1));
+ EXPECT_TRUE(p->CanReadFileForBrowserUpload(file2));
+
+ p->RevokeFileForBrowserUpload(token2);
+ EXPECT_FALSE(p->CanReadFileForBrowserUpload(file1));
+ EXPECT_FALSE(p->CanReadFileForBrowserUpload(file2));
+}
+
+TEST_P(ChildProcessSecurityPolicyTest,
+ BrowserFileAccess_OneTokenMultipleFiles) {
+ auto* p = ChildProcessSecurityPolicyImpl::GetInstance();
+ base::FilePath file1(TEST_PATH("/foo/bar1"));
+ base::FilePath file2(TEST_PATH("/foo/bar2"));
+ base::UnguessableToken token = base::UnguessableToken::Create();
+
+ p->GrantFileForBrowserUpload(token, file1);
+ p->GrantFileForBrowserUpload(token, file2);
+
+ EXPECT_TRUE(p->CanReadFileForBrowserUpload(file1));
+ EXPECT_TRUE(p->CanReadFileForBrowserUpload(file2));
+
+ p->RevokeFileForBrowserUpload(token);
+ EXPECT_FALSE(p->CanReadFileForBrowserUpload(file1));
+ EXPECT_FALSE(p->CanReadFileForBrowserUpload(file2));
+}
+
+TEST_P(ChildProcessSecurityPolicyTest,
+ BrowserFileAccess_RevokeNonExistentToken) {
+ auto* p = ChildProcessSecurityPolicyImpl::GetInstance();
+ base::FilePath file(TEST_PATH("/foo/bar"));
+ base::UnguessableToken valid_token = base::UnguessableToken::Create();
+ base::UnguessableToken invalid_token = base::UnguessableToken::Create();
+
+ p->GrantFileForBrowserUpload(valid_token, file);
+ EXPECT_TRUE(p->CanReadFileForBrowserUpload(file));
+
+ // Revoking a token that was never granted access shouldn't crash or modify
+ // other grants.
+ p->RevokeFileForBrowserUpload(invalid_token);
+ EXPECT_TRUE(p->CanReadFileForBrowserUpload(file));
+
+ p->RevokeFileForBrowserUpload(valid_token);
+ EXPECT_FALSE(p->CanReadFileForBrowserUpload(file));
+}
+
// This intentionally excludes {kCppOnly, kProcessState} since that behaves
// the same as {kCppOnly, kMain} and is redundant to test separately.
const CpspTestParam kCpspTestParams[] = {
Original Bug Report
Sandbox Escape: Arbitrary File Read via forged OriginatingProcessId in Network Service
Project Fortify, an experimental security project, has identified the following potential security issue.
Overview: A compromised Network Service can potentially bypass file access checks and read arbitrary host files. By forging the OriginatingProcessId parameter in the NetworkContextClient::OnFileUploadRequested Mojo IPC, the Network Service can trick the browser process into skipping ChildProcessSecurityPolicy validation, returning open handles for sensitive files.
Affected files:
content/browser/network_context_client_base_impl.cccontent/browser/storage_partition_impl.ccservices/network/public/mojom/originating_process_id.mojom
Estimated timestamp from git blame: 2026-02-25
Summary
A logical flaw in the handling of file upload requests allows a compromised Network Service to read arbitrary files, leading to a potential sandbox escape. The NetworkContextClient::OnFileUploadRequested Mojo interface allows the Network Service to request the browser process to open files for upload. This method accepts an OriginatingProcessId parameter. Because the browser process explicitly trusts this parameter without secondary validation, a compromised Network Service can claim to be the browser process, thereby bypassing ChildProcessSecurityPolicy checks and obtaining readable file handles to any file on the host OS.
Technical Details
The vulnerability exists within content/browser/network_context_client_base_impl.cc. When the Network Service sends an OnFileUploadRequested IPC, the request is eventually handled by HandleFileUploadRequest.
void HandleFileUploadRequest(
network::OriginatingProcessId process_id,
// ...
const std::vector<base::FilePath>& file_paths,
/* ... */) {
// ...
ChildProcessSecurityPolicy* cpsp = ChildProcessSecurityPolicy::GetInstance();
for (const auto& file_path : file_paths) {
if (!process_id.is_browser() &&
!cpsp->CanReadFile(ToChildProcessId(process_id.renderer_process_id()),
file_path)) {
// Access Denied
return;
}
// File is opened with base::File::FLAG_OPEN | base::File::FLAG_READ
files.emplace_back(file_path, file_flags);
// ...
}
// Returns the opened file handles to the Network Service over IPC
}
The process_id parameter is fully controlled by the Network Service. If the Network Service populates the union such that process_id.is_browser() returns true, the first half of the logical && condition evaluates to false. Due to C++ short-circuit evaluation, cpsp->CanReadFile(...) is never called.
As a result, the unsandboxed browser process opens the requested file paths with read privileges and returns the resulting base::File handles to the Network Service. On platforms like macOS, Windows, and Linux where the Network Service is sandboxed, this allows the attacker to silently escape the sandbox and exfiltrate sensitive files (e.g., ~/.ssh/id_rsa, Login Data, etc.).
(Note: These are suggested/potential steps based on source code analysis, as our tooling agent does not yet have the ability to run code or execute a live proof-of-concept).
Suggested Attacker Steps
- Exploit a vulnerability (e.g., memory corruption) in the Network Service to achieve arbitrary code execution within its sandboxed process.
- Obtain the existing
pending_remote<NetworkContextClient>Mojo endpoint, which the browser passed to the Network Service upon initialization. - Construct an
OnFileUploadRequestedMojo message. Set theprocess_idparameter toOriginatingProcessId::browser(). - Populate the
file_pathsarray with absolute paths to sensitive host files. - Send the IPC to the browser process and wait for the callback.
- Retrieve the
ReadOnlyFilehandles from the IPC callback response and read the file contents directly into the Network Service.
Suggested Fix
The browser process must not trust the OriginatingProcessId provided by the Network Service to bypass critical security checks, as the Network Service itself is sandboxed and considered a lesser-trusted domain than the browser process.
To fix this:
- Do not allow the Network Service to request file opens on behalf of the browser. If the browser initiates a network request involving local files, the browser should open the files itself and pass the resulting file handles (or a
DataPipe) down to the Network Service, rather than passing a file path and expecting the Network Service to ask the browser to open it later. - Alternatively, if the architecture strictly requires this round-trip, securely validate that the request originated from the browser process. This could be done by associating requests with secure, unguessable tokens generated by the browser when the upload was initially dispatched, ensuring the Network Service cannot forge a browser-originated request.
Evaluated with Chrome root at commit: 876d480da1f794d87813cfa2e6ff4fcf9771e939
Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; 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.