High watch CVSS 8.3 chrome Input Validation 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
8.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient validation of untrusted input in Network
DescriptionInsufficient validation of untrusted input in Network
ComponentNetwork
Bug ClassInput Validation
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.
Tracker497428001
Fix commit1fc7b5f1b300 (chromium/src) +368/-15
CWECWE-20 (Improper input validation)
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:C/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-03-29 (per vendor advisory)
Fix landed2026-07-06 (public git)
Disclosed2026-07-29
Patch gap23 days — fix public in source before the advisory

Background

Browser-initiated file upload
a network request (e.g. Web Share Target) whose request_body references local base::FilePath files, submitted to the network service on behalf of the browser process rather than a renderer.
`ChildProcessSecurityPolicy`
the browser-side authority that tracks which files each process is permitted to read via checks like CanReadFile().
`OriginatingProcessId`
the identifier the network context client uses to distinguish a browser-originated file upload (is_browser()) from a renderer-originated one.
`kBrowserInitiatedFileUploadValidation`
the feature flag, enabled by default, that gates the new token-based file-access registration and validation path.

Root Cause Analysis

When NetworkContextClientBaseImpl::HandleFileUploadRequest (via OnFileUploadRequested) processed an upload, it only ran cpsp->CanReadFile() for renderer processes; when process_id.is_browser() was true it bypassed all validation and opened every requested base::FilePath unconditionally. This meant any file path that reached the network service under the browser’s OriginatingProcessId was read with the browser’s full privilege, so a compromised process path feeding browser-tagged upload requests could reference files the browser had no legitimate reason to open.

The fix introduces token-scoped tracking: ScopedBrowserFileAccess registers the referenced files with ChildProcessSecurityPolicy for the navigation’s lifetime, and the client now calls cpsp->CanReadFileForBrowserUpload(file_path) for browser-originated uploads, denying with net::ERR_ACCESS_DENIED any path that was not explicitly granted. Because the grant is created only for files legitimately referenced by a navigation’s request_body and revoked when the loader is destroyed, arbitrary browser-tagged paths no longer satisfy the check.

Key insight
The single mistake was treating the browser OriginatingProcessId as inherently trusted and skipping file-access validation entirely for it; the fix replaces that blanket trust with an explicit per-file token grant (CanReadFileForBrowserUpload()) so browser-initiated uploads must also prove authorization.

Attack Path

  1. Reach the browser upload path Drive a browser-initiated upload flow such as Web Share Target, where files are attached to a navigation request_body and forwarded to the network service with OriginatingProcessId::browser().
  2. Exploit the trust bypass In the pre-fix for (const auto& file_path : file_paths) loop, process_id.is_browser() short-circuits the CanReadFile() check, so no per-file authorization is enforced.
  3. Reference unauthorized files Supply or influence file paths that the browser should not read; because validation is skipped, the network service opens them with browser privilege.
  4. Exfiltrate contents The opened base::File handles are returned and their contents are sent as part of the upload request body to an attacker-controllable destination.

Impact Assessment

An attacker who can influence the file paths in a browser-initiated upload gains the ability to have privileged local files opened and exfiltrated through the network service, since the browser process path was exempt from CanReadFile() validation. This operates in the browser/network-service context with the browser’s file-read authority; the precondition is a code path (e.g. Web Share Target) that submits attacker-influenced base::FilePath values under OriginatingProcessId::browser().

Changed Functions

FunctionChangeNotes
BindRepeating
content/browser/browser_main_loop.cc
modified
for
content/browser/network_context_client_base_impl.cc
modified
if
content/browser/network_context_client_base_impl.cc
modified
TEST_F
content/browser/network_context_client_base_impl_unittest.cc
modified

Files Changed

  • content/browser/BUILD.gn
  • content/browser/browser_main_loop.cc
  • content/browser/loader/navigation_url_loader_impl.cc
  • content/browser/loader/navigation_url_loader_impl.h
  • content/browser/network_context_client_base_impl.cc
  • content/browser/network_context_client_base_impl_unittest.cc

Audit Directions

  • Process-identity trust shortcuts
    Search for authorization checks skipped when a request claims to originate from the browser (is_browser()), where the code assumes browser origin equals trusted input.
  • Unvalidated `base::FilePath` to network service
    Audit other paths that forward file paths or request_body referenced files to the network service without a corresponding ChildProcessSecurityPolicy grant/validation.
  • Scoped-grant lifetime gaps
    Review ScopedBrowserFileAccess-style grants to confirm every register (GrantFileForBrowserUpload) is paired with a revoke on loader destruction, so access cannot outlive the navigation that justified it.
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();
Loading diff…

Regression Test / PoC

shipped with the fix
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[] = {
Loading diff…

Original Bug Report

reported by rj...@google.com

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.cc
  • content/browser/storage_partition_impl.cc
  • services/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

  1. Exploit a vulnerability (e.g., memory corruption) in the Network Service to achieve arbitrary code execution within its sandboxed process.
  2. Obtain the existing pending_remote<NetworkContextClient> Mojo endpoint, which the browser passed to the Network Service upon initialization.
  3. Construct an OnFileUploadRequested Mojo message. Set the process_id parameter to OriginatingProcessId::browser().
  4. Populate the file_paths array with absolute paths to sensitive host files.
  5. Send the IPC to the browser process and wait for the callback.
  6. Retrieve the ReadOnlyFile handles 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:

  1. 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.
  2. 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.

View on issue tracker