CVE-2026-79155
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
TEST_Fgoogle_apis/drive/drive_base_requests_server_unittest.cc |
modified |
Files Changed
google_apis/common/base_requests.ccgoogle_apis/drive/drive_base_requests_server_unittest.cc
Patch
From 8d02bbd914aaf89947d8055e10f41d45eab99fc2 Mon Sep 17 00:00:00 2001
From: Fergal Daly <fergal@chromium.org>
Date: Thu, 02 Jul 2026 02:33:00 -0700
Subject: [PATCH] google_apis: Do not follow symlinks when writing download output
UrlFetchRequestBase::WriteFileData() opens the caller-supplied output
path some time after the caller may have created it as a temporary file.
On POSIX, open the path with O_NOFOLLOW so that the write fails if the
final path component has been replaced by a symbolic link in the
meantime, and the request is cancelled with OTHER_ERROR instead of
writing through the link.
Add a DownloadFileRequestBase regression test that supplies a symlink as
the output path and verifies that the link target is left untouched and
the request reports failure.
Fixed: 522294538
Change-Id: Ia436d9f66fbb36d221d437a3a507c03e8cad9b04
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8029006
Reviewed-by: Mingyu Lei <leimy@chromium.org>
Auto-Submit: Fergal Daly <fergal@chromium.org>
Reviewed-by: Fergal Daly <fergal@chromium.org>
Commit-Queue: Fergal Daly <fergal@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1655969}
---
diff --git a/google_apis/common/base_requests.cc b/google_apis/common/base_requests.cc
index 4ac59d3..037901c 100644
--- a/google_apis/common/base_requests.cc
+++ b/google_apis/common/base_requests.cc
@@ -13,6 +13,7 @@
#include <utility>
#include "base/containers/span.h"
+#include "base/files/file.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/json/json_reader.h"
@@ -21,6 +22,7 @@
#include "base/task/sequenced_task_runner.h"
#include "base/task/single_thread_task_runner.h"
#include "base/values.h"
+#include "build/build_config.h"
#include "google_apis/common/request_sender.h"
#include "google_apis/common/task_util.h"
#include "google_apis/credentials_mode.h"
@@ -31,6 +33,15 @@
#include "services/network/public/cpp/resource_request.h"
#include "services/network/public/mojom/url_response_head.mojom.h"
+#if BUILDFLAG(IS_POSIX)
+#include <fcntl.h>
+#include <sys/stat.h>
+
+#include "base/files/scoped_file.h"
+#include "base/posix/eintr_wrapper.h"
+#include "base/threading/scoped_blocking_call.h"
+#endif
+
namespace {
// Template for optional OAuth2 authorization HTTP header.
@@ -336,9 +347,24 @@
bool UrlFetchRequestBase::WriteFileData(std::string file_data,
DownloadData* download_data) {
if (!download_data->output_file.IsValid()) {
+#if BUILDFLAG(IS_POSIX)
+ // The output path may refer to a temporary file that was created in
+ // advance and is being reopened here, so do not follow symbolic links.
+ base::ScopedBlockingCall scoped_blocking_call(
+ FROM_HERE, base::BlockingType::MAY_BLOCK);
+ base::ScopedFD fd(
+ HANDLE_EINTR(open(download_data->output_file_path.value().c_str(),
+ O_CREAT | O_TRUNC | O_WRONLY | O_NOFOLLOW | O_CLOEXEC,
+ S_IRUSR | S_IWUSR)));
+ if (!fd.is_valid()) {
+ return false;
+ }
+ download_data->output_file = base::File(std::move(fd));
+#else
download_data->output_file.Initialize(
download_data->output_file_path,
base::File::FLAG_CREATE_ALWAYS | base::File::FLAG_WRITE);
+#endif
if (!download_data->output_file.IsValid())
return false;
}
diff --git a/google_apis/drive/drive_base_requests_server_unittest.cc b/google_apis/drive/drive_base_requests_server_unittest.cc
index 8f3fc55..d80f5c1 100644
--- a/google_apis/drive/drive_base_requests_server_unittest.cc
+++ b/google_apis/drive/drive_base_requests_server_unittest.cc
@@ -2,8 +2,6 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
-#include "google_apis/drive/drive_base_requests.h"
-
#include <memory>
#include "base/files/file_util.h"
@@ -11,10 +9,12 @@
#include "base/functional/bind.h"
#include "base/run_loop.h"
#include "base/test/task_environment.h"
+#include "build/build_config.h"
#include "google_apis/common/dummy_auth_service.h"
#include "google_apis/common/request_sender.h"
#include "google_apis/common/task_util.h"
#include "google_apis/common/test_util.h"
+#include "google_apis/drive/drive_base_requests.h"
#include "mojo/public/cpp/bindings/pending_remote.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
@@ -166,4 +166,39 @@
// Do not verify the not found message.
}
+#if BUILDFLAG(IS_POSIX)
+TEST_F(BaseRequestsServerTest, DownloadFileRequest_SymlinkOutputPath) {
+ const std::string kTargetContents = "must not be overwritten";
+ const base::FilePath target_path =
+ GetTestCachedFilePath(base::FilePath::FromUTF8Unsafe("symlink_target"));
+ ASSERT_TRUE(base::WriteFile(target_path, kTargetContents));
+
+ const base::FilePath link_path =
+ GetTestCachedFilePath(base::FilePath::FromUTF8Unsafe("symlink_output"));
+ ASSERT_TRUE(base::CreateSymbolicLink(target_path, link_path));
+
+ ApiErrorCode result_code = OTHER_ERROR;
+ base::FilePath temp_file;
+ {
+ base::RunLoop run_loop;
+ std::unique_ptr<DownloadFileRequestBase> request =
+ std::make_unique<DownloadFileRequestBase>(
+ request_sender_.get(),
+ test_util::CreateQuitCallback(
+ &run_loop,
+ test_util::CreateCopyResultCallback(&result_code, &temp_file)),
+ GetContentCallback(), ProgressCallback(),
+ test_server_.GetURL("/files/drive/testfile.txt"), link_path);
+ request_sender_->StartRequestWithAuthRetry(std::move(request));
+ run_loop.Run();
+ }
+
+ EXPECT_NE(HTTP_SUCCESS, result_code);
+
+ std::string target_contents;
+ ASSERT_TRUE(base::ReadFileToString(target_path, &target_contents));
+ EXPECT_EQ(kTargetContents, target_contents);
+}
+#endif // BUILDFLAG(IS_POSIX)
+
} // namespace google_apis
Regression Test / PoC
diff --git a/google_apis/drive/drive_base_requests_server_unittest.cc b/google_apis/drive/drive_base_requests_server_unittest.cc
index 8f3fc55..d80f5c1 100644
--- a/google_apis/drive/drive_base_requests_server_unittest.cc
+++ b/google_apis/drive/drive_base_requests_server_unittest.cc
@@ -2,8 +2,6 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
-#include "google_apis/drive/drive_base_requests.h"
-
#include <memory>
#include "base/files/file_util.h"
@@ -11,10 +9,12 @@
#include "base/functional/bind.h"
#include "base/run_loop.h"
#include "base/test/task_environment.h"
+#include "build/build_config.h"
#include "google_apis/common/dummy_auth_service.h"
#include "google_apis/common/request_sender.h"
#include "google_apis/common/task_util.h"
#include "google_apis/common/test_util.h"
+#include "google_apis/drive/drive_base_requests.h"
#include "mojo/public/cpp/bindings/pending_remote.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
@@ -166,4 +166,39 @@
// Do not verify the not found message.
}
+#if BUILDFLAG(IS_POSIX)
+TEST_F(BaseRequestsServerTest, DownloadFileRequest_SymlinkOutputPath) {
+ const std::string kTargetContents = "must not be overwritten";
+ const base::FilePath target_path =
+ GetTestCachedFilePath(base::FilePath::FromUTF8Unsafe("symlink_target"));
+ ASSERT_TRUE(base::WriteFile(target_path, kTargetContents));
+
+ const base::FilePath link_path =
+ GetTestCachedFilePath(base::FilePath::FromUTF8Unsafe("symlink_output"));
+ ASSERT_TRUE(base::CreateSymbolicLink(target_path, link_path));
+
+ ApiErrorCode result_code = OTHER_ERROR;
+ base::FilePath temp_file;
+ {
+ base::RunLoop run_loop;
+ std::unique_ptr<DownloadFileRequestBase> request =
+ std::make_unique<DownloadFileRequestBase>(
+ request_sender_.get(),
+ test_util::CreateQuitCallback(
+ &run_loop,
+ test_util::CreateCopyResultCallback(&result_code, &temp_file)),
+ GetContentCallback(), ProgressCallback(),
+ test_server_.GetURL("/files/drive/testfile.txt"), link_path);
+ request_sender_->StartRequestWithAuthRetry(std::move(request));
+ run_loop.Run();
+ }
+
+ EXPECT_NE(HTTP_SUCCESS, result_code);
+
+ std::string target_contents;
+ ASSERT_TRUE(base::ReadFileToString(target_path, &target_contents));
+ EXPECT_EQ(kTargetContents, target_contents);
+}
+#endif // BUILDFLAG(IS_POSIX)
+
} // namespace google_apis
Original Bug Report
Potential macOS RemoteToLocalSyncer TOCTOU allows arbitrary file write and sandbox escape
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 TOCTOU vulnerability in RemoteToLocalSyncer::DownloadFile on macOS allows a compromised sandboxed process to achieve arbitrary file truncation and write operations at browser-process privilege. This occurs because the browser process discards the safe file descriptor of a generated temporary file, subsequently re-opening the path without O_NOFOLLOW. Consequently, a compromised child process with write permissions to the shared temporary directory could swap the file for a symbolic link during the network download round-trip.
Affected files:
chrome/browser/sync_file_system/drive_backend/remote_to_local_syncer.ccgoogle_apis/common/base_requests.ccchrome/browser/sync_file_system/sync_file_system_service.cc
Estimated timestamp from git blame: 2013-11-12
Detailed Description
A potential Time-of-Check to Time-of-Use (TOCTOU) vulnerability has been identified in the RemoteToLocalSyncer::DownloadFile function on macOS. The issue stems from the way temporary download files are managed and subsequently populated with network data.
Potential Vulnerability Mechanism
-
Temporary File Creation and FD Discarding:
RemoteToLocalSyncer::DownloadFilecreates a temporary file under the system temporary directory (which resolves to_CS_DARWIN_USER_TEMP_DIRviaNSTemporaryDirectory()on macOS) by callingCreateTemporaryFile:// chrome/browser/sync_file_system/drive_backend/remote_to_local_syncer.cc storage::ScopedFile file = CreateTemporaryFile(...); base::FilePath path = file.path();The underlying implementation in
base::CreateTemporaryFileInDirgenerates the path, invokesmkstemp(), and immediately closes the safe file descriptor, returning only the raw path to the caller. -
Network Process Round-Trip and Path Re-opening: The naked file path is passed to the Drive download stack (
drive_service()->DownloadFile). TheUrlFetchRequestBasestreams the download via the network service. When data is received, the browser process re-opens the file path on its blocking task runner using thebase::File::FLAG_CREATE_ALWAYSflag:// google_apis/common/base_requests.cc download_data->output_file.Initialize( download_data->output_file_path, base::File::FLAG_CREATE_ALWAYS | base::File::FLAG_WRITE); -
Unsafe POSIX open() Flags: In
base/files/file_posix.cc,FLAG_CREATE_ALWAYSis translated toO_CREAT | O_TRUNC(withO_WRONLY), but does not includeO_NOFOLLOW:if (flags & FLAG_CREATE_ALWAYS) { open_flags = O_CREAT | O_TRUNC; }
Potential Attack Path (Theoretical Analysis)
Because the automated security tooling does not have the capability to execute code or verify runtime behavior, the following is a theoretical sequence of steps an attacker might follow to exploit this behavior:
- An attacker first obtains code execution within a sandboxed child process (such as the Network Process or GPU Process) on macOS.
- Both the unsandboxed browser process and the sandboxed child processes share access to the per-user temporary directory (
_CS_DARWIN_USER_TEMP_DIR). The child processes havefile-write*permissions (which allowunlinkandsymlinkoperations) on this directory. - The attacker monitors the temporary directory for files conforming to the temporary template (e.g.,
.<bundle-id>.XXXXXX). - Upon detection, the attacker unlinks the temporary file and creates a symbolic link at that path pointing to a sensitive file owned by the victim (for example,
~/.zshrc). - When the browser process receives the download data stream, it opens the path via
open(). WithoutO_NOFOLLOW, the kernel follows the symbolic link, leading to the truncation and overwrite of the target user file (~/.zshrc) with the downloaded content.
If the attacker controls the downloaded content (which is the case if they have compromised the Network Process and can spoof or modify response streams), they can achieve arbitrary code execution outside of the sandbox upon the next shell initiation.
Suggested Fix
To resolve this potential vulnerability, the file descriptor returned by the temporary file creation function should be retained and passed directly to the writer, avoiding any name-based re-opening. Alternatively, if name-based re-opening is necessary, the file must be opened using safety flags such as O_NOFOLLOW or the directory must be exclusive to the browser process and inaccessible to sandboxed child processes.
Evaluated with Chrome root at commit: b2fea2e31df308d0f04e4ae47def4c4f939ee141
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.