Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInappropriate implementation in Network
DescriptionInappropriate implementation in Network
ComponentNetwork
Bug ClassLogic Error
Tracker519991751
Fix commit0ad3c50861b7 (chromium/src) +41/-2
CISA KEVNot listed
CreditedGoogle
Disclosed2026-07-29

Changed Functions

FunctionChangeNotes
TEST_F
services/network/public/cpp/simple_url_loader_unittest.cc
modified
for
services/network/public/cpp/simple_url_loader_unittest.cc
modified

Files Changed

  • services/network/public/cpp/simple_url_loader.cc
  • services/network/public/cpp/simple_url_loader_unittest.cc
From 0ad3c50861b7d3f0d214b7d93af6728738c5bca5 Mon Sep 17 00:00:00 2001
From: Bryan Oltman <bryanoltman@google.com>
Date: Tue, 16 Jun 2026 13:23:32 -0700
Subject: [PATCH] Prevent downloading to symlinks in SimpleURLLoader

When SimpleURLLoader downloads to a user-specified file path, initialize
the file with base::File::FLAG_NO_FOLLOW. This prevents the loader from
resolving and overwriting the target of a symbolic link.

Fixed: 519991751
Change-Id: I72b6a516820af8cead6ddb494e393e63a59c8d0f
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7944745
Commit-Queue: Bryan Oltman <bryanoltman@google.com>
Reviewed-by: Reilly Grant <reillyg@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1647848}
---

diff --git a/services/network/public/cpp/simple_url_loader.cc b/services/network/public/cpp/simple_url_loader.cc
index 1ac2f96..1ceeb607 100644
--- a/services/network/public/cpp/simple_url_loader.cc
+++ b/services/network/public/cpp/simple_url_loader.cc
@@ -1026,8 +1026,9 @@
           file_ = base::CreateAndOpenTemporaryFileInDir(temp_dir, &path_);
         }
       } else {
-        file_.Initialize(
-            path_, base::File::FLAG_WRITE | base::File::FLAG_CREATE_ALWAYS);
+        file_.Initialize(path_, base::File::FLAG_WRITE |
+                                    base::File::FLAG_CREATE_ALWAYS |
+                                    base::File::FLAG_NO_FOLLOW);
       }
 
       // If CreateTemporaryFile() or File::Initialize() failed, report failure.
diff --git a/services/network/public/cpp/simple_url_loader_unittest.cc b/services/network/public/cpp/simple_url_loader_unittest.cc
index f2f2cce..9356b0a 100644
--- a/services/network/public/cpp/simple_url_loader_unittest.cc
+++ b/services/network/public/cpp/simple_url_loader_unittest.cc
@@ -3565,6 +3565,44 @@
   EXPECT_FALSE(test_helper->response_body());
 }
 
+#if BUILDFLAG(IS_POSIX)
+// Make sure that downloading to a path that is a symbolic link fails (does not
+// overwrite the target of the symlink).
+TEST_F(SimpleURLLoaderFileTest, DownloadToSymlinkFails) {
+  base::ScopedAllowBlockingForTesting allow_blocking;
+  base::ScopedTempDir target_dir;
+  ASSERT_TRUE(target_dir.CreateUniqueTempDir());
+
+  // Create a target file (sensitive file) containing "sensitive data"
+  base::FilePath target_file =
+      target_dir.GetPath().AppendASCII("sensitive_target.txt");
+  std::string sensitive_data = "sensitive data";
+  ASSERT_TRUE(base::WriteFile(target_file, sensitive_data));
+
+  // Create a symlink pointing to the target file
+  std::unique_ptr<SimpleLoaderTestHelper> test_helper =
+      CreateHelperForURL(test_server_.GetURL("/echo"));
+  base::FilePath symlink_path = test_helper->dest_path();
+
+  // Make sure dest_path doesn't exist before we create the symlink
+  base::DeleteFile(symlink_path);
+
+  ASSERT_TRUE(base::CreateSymbolicLink(target_file, symlink_path));
+
+  // Start downloading to the symlink path
+  test_helper->set_expect_path_exists_on_error(true);
+  test_helper->StartSimpleLoaderAndWait(url_loader_factory_.get());
+
+  // Verify that:
+  // - The download failed or was rejected.
+  // - The target file was NOT overwritten/modified.
+  EXPECT_NE(net::OK, test_helper->simple_url_loader()->NetError());
+  std::string actual_data;
+  ASSERT_TRUE(base::ReadFileToString(target_file, &actual_data));
+  EXPECT_EQ(sensitive_data, actual_data);
+}
+#endif  // BUILDFLAG(IS_POSIX)
+
 // Make sure that destroying the loader destroys a partially downloaded file.
 TEST_F(SimpleURLLoaderFileTest, DeleteLoaderDuringRequestDestroysFile) {
   for (bool body_data_read : {false, true}) {
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/services/network/public/cpp/simple_url_loader_unittest.cc b/services/network/public/cpp/simple_url_loader_unittest.cc
index f2f2cce..9356b0a 100644
--- a/services/network/public/cpp/simple_url_loader_unittest.cc
+++ b/services/network/public/cpp/simple_url_loader_unittest.cc
@@ -3565,6 +3565,44 @@
   EXPECT_FALSE(test_helper->response_body());
 }
 
+#if BUILDFLAG(IS_POSIX)
+// Make sure that downloading to a path that is a symbolic link fails (does not
+// overwrite the target of the symlink).
+TEST_F(SimpleURLLoaderFileTest, DownloadToSymlinkFails) {
+  base::ScopedAllowBlockingForTesting allow_blocking;
+  base::ScopedTempDir target_dir;
+  ASSERT_TRUE(target_dir.CreateUniqueTempDir());
+
+  // Create a target file (sensitive file) containing "sensitive data"
+  base::FilePath target_file =
+      target_dir.GetPath().AppendASCII("sensitive_target.txt");
+  std::string sensitive_data = "sensitive data";
+  ASSERT_TRUE(base::WriteFile(target_file, sensitive_data));
+
+  // Create a symlink pointing to the target file
+  std::unique_ptr<SimpleLoaderTestHelper> test_helper =
+      CreateHelperForURL(test_server_.GetURL("/echo"));
+  base::FilePath symlink_path = test_helper->dest_path();
+
+  // Make sure dest_path doesn't exist before we create the symlink
+  base::DeleteFile(symlink_path);
+
+  ASSERT_TRUE(base::CreateSymbolicLink(target_file, symlink_path));
+
+  // Start downloading to the symlink path
+  test_helper->set_expect_path_exists_on_error(true);
+  test_helper->StartSimpleLoaderAndWait(url_loader_factory_.get());
+
+  // Verify that:
+  // - The download failed or was rejected.
+  // - The target file was NOT overwritten/modified.
+  EXPECT_NE(net::OK, test_helper->simple_url_loader()->NetError());
+  std::string actual_data;
+  ASSERT_TRUE(base::ReadFileToString(target_file, &actual_data));
+  EXPECT_EQ(sensitive_data, actual_data);
+}
+#endif  // BUILDFLAG(IS_POSIX)
+
 // Make sure that destroying the loader destroys a partially downloaded file.
 TEST_F(SimpleURLLoaderFileTest, DeleteLoaderDuringRequestDestroysFile) {
   for (bool body_data_read : {false, true}) {
Loading diff…

Original Bug Report

reported by vm...@google.com

Potential Sandbox Escape on macOS via base::ScopedTempFile and SimpleURLLoader TOCTOU

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 potential systemic Time-of-Check to Time-of-Use (TOCTOU) vulnerability exists when using base::ScopedTempFile in conjunction with path-based reopens (such as SimpleURLLoader::DownloadToFile). Because macOS GPU and Network sandbox profiles permit write and unlink access to the shared temporary directory, a compromised child process could replace the temporary file with a symbolic link. Subsequent write operations by the unsandboxed browser process would then follow the symlink, leading to arbitrary file write or truncation outside the sandbox.

Affected files:

  • base/files/scoped_temp_file.cc
  • base/files/scoped_temp_file.h
  • base/files/file_proxy.cc
  • chrome/browser/enterprise/data_protection/file_snapshot_creator.cc
  • components/webapps/isolated_web_apps/download/bundle_downloader.cc
  • services/network/public/cpp/simple_url_loader.cc

Estimated timestamp from git blame: 2023-09-20

Potential Systemic TOCTOU vulnerability in base::ScopedTempFile and SimpleURLLoader on macOS

Summary

A potential design flaw in base::ScopedTempFile exposes callers to path-based reopening races. On macOS, sandboxed child processes (e.g., GPU or Network processes) share access to the system temporary directory (darwin-user-temp-dir). Because base::ScopedTempFile closes its file descriptor immediately after creation and only maintains the string path, a compromised child process can race to replace the temp file with a symlink. When the browser process subsequently reopens the file path (for example, via SimpleURLLoader::DownloadToFile with create_temp_file = false), it uses base::File::Initialize with FLAG_CREATE_ALWAYS (equivalent to POSIX O_CREAT | O_TRUNC) without O_NOFOLLOW. This causes the unsandboxed browser process to follow the symlink and write or truncate arbitrary files at browser-process privilege levels, resulting in a potential sandbox escape.

Potential Vulnerability Mechanism

  1. Creation: A high-privilege component in the browser process (such as IsolatedWebAppUpdateDiscoveryTask during IWA updates or IwaInstaller during policy-enforced installation) initiates a temporary file allocation using ScopedTempWebBundleFile::Create() (components/webapps/isolated_web_apps/download/bundle_downloader.cc:25).
  2. Handle Release: Under the hood, base::ScopedTempFile::Create() uses base::CreateTemporaryFile which executes mkstemp (base/files/file_util_posix.cc:865). It returns a ScopedFD which is immediately destroyed on scope exit, closing the secure file handle. The class only retains the string path (base/files/scoped_temp_file.cc:37-40).
  3. The Race: Under macOS sandbox policies, sandboxed child processes (such as GPU or Network) are granted file-write-unlink and file-write-create access to the shared subpath darwin-user-temp-dir (sandbox/policy/mac/gpu.sb:124-128). A compromised process can monitor the temporary directory, detect the creation of the temporary file, unlink() it, and place a symlink at the same path pointing to a sensitive file (e.g., ~/Library/LaunchAgents/pwn.plist).
  4. Reopening and Write: The browser process instructs SimpleURLLoader::DownloadToFile to write downloaded data to the ScopedTempWebBundleFile’s path. Because an explicit path is provided, the network service invokes SaveToFileBodyHandler::FileWriter with create_temp_file_ = false (services/network/public/cpp/simple_url_loader.cc:1349-1352).
  5. Unchecked Open: FileWriter opens the target file using base::File::Initialize with base::File::FLAG_CREATE_ALWAYS (services/network/public/cpp/simple_url_loader.cc:1029), which compiles to open(..., O_CREAT | O_TRUNC | O_WRONLY) (base/files/file_posix.cc:573-577). Crucially, O_NOFOLLOW is not specified. The kernel follows the symlink, truncating and overwriting the target file outside the sandbox at browser-process privilege level.

Note: Our static analysis tools cannot execute code or generate live proofs of concept, so these steps represent a potential attack sequence derived from manual code tracing.

Impact

If successfully exploited, a compromised GPU or Network child process could escape the macOS App Sandbox by writing to user configuration files (such as LaunchAgents), executing arbitrary code at the user’s full privilege level.

Suggested Remediation

  1. Harden base::File::DoInitialize to restrict symlink traversal when opening/creating temporary files or when FLAG_CREATE_ALWAYS is specified, possibly by leveraging O_NOFOLLOW or verifying file descriptors using fstat.
  2. Deprecate path-only temporary file structures (like base::ScopedTempFile) in favor of classes that retain and pass an open base::File or ScopedFD handle (such as CreateAndOpenTemporaryFileInDir), eliminating the necessity of performing path-based reopens in the browser process.

Evaluated with Chrome root at commit: 57b021e1fdae94a215627d29aeb1ccf2eb5b3e91


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