Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactIncorrect reference resolution in FileSystem
DescriptionIncorrect reference resolution in FileSystem
ComponentFileSystem
Bug ClassLogic Error
Tracker500038021
Fix commit835fc99abf93 (chromium/src) +127/-4
CISA KEVNot listed
CreditedGoogle
Disclosed2026-08-25

Changed Functions

FunctionChangeNotes
TEST_F
storage/browser/file_system/local_file_util_unittest.cc
modified

Files Changed

  • storage/browser/file_system/local_file_util.cc
  • storage/browser/file_system/local_file_util_unittest.cc
  • storage/browser/test/test_file_system_backend.cc
From 835fc99abf93cf8213729a7801604f284f5fa709 Mon Sep 17 00:00:00 2001
From: Eriko Kurimoto <[email protected]>
Date: Wed, 08 Jul 2026 00:25:14 -0700
Subject: [PATCH] storage: Reject symlinks in remaining LocalFileUtil operations

LocalFileUtil::IsHiddenItem() (which checks for symbolic links) was
applied in CreateOrOpen(), GetFileInfo() and the file enumerator, but
not in EnsureFileExists(), Touch(), Truncate(), CopyOrMoveFile() or
CopyInForeignFile(). This made those operations inconsistent with the
rest of the class, since symlinks are intentionally not exposed through
this util.

Add the missing IsHiddenItem() checks so that all write operations
consistently return FILE_ERROR_NOT_FOUND for symbolic links, matching
the existing behaviour of CreateOrOpen() and GetFileInfo().

Bug: 500038021
Change-Id: I6802cfd66f9ca6607c36e1c60d457b8fb52b5867
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8033621
Reviewed-by: Mingyu Lei <[email protected]>
Commit-Queue: Mingyu Lei <[email protected]>
Auto-Submit: Eriko Kurimoto <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1658543}
---

diff --git a/storage/browser/file_system/local_file_util.cc b/storage/browser/file_system/local_file_util.cc
index 9f280b41..766cf3e 100644
--- a/storage/browser/file_system/local_file_util.cc
+++ b/storage/browser/file_system/local_file_util.cc
@@ -104,8 +104,6 @@
   base::File::Error error = GetLocalFilePath(context, url, &file_path);
   if (error != base::File::FILE_OK)
     return base::File(error);
-  if (IsHiddenItem(file_path))
-    return base::File(base::File::FILE_ERROR_NOT_FOUND);
 
   return NativeFileUtil::CreateOrOpen(file_path, file_flags);
 }
@@ -142,8 +140,6 @@
   base::File::Error error = GetLocalFilePath(context, url, &file_path);
   if (error != base::File::FILE_OK)
     return error;
-  if (IsHiddenItem(file_path))
-    return base::File::FILE_ERROR_NOT_FOUND;
 
   error = NativeFileUtil::GetFileInfo(file_path, file_info);
   if (error == base::File::FILE_OK)
@@ -175,6 +171,9 @@
     return base::File::FILE_ERROR_ACCESS_DENIED;
   }
   *local_file_path = url.path();
+  if (IsHiddenItem(*local_file_path)) {
+    return base::File::FILE_ERROR_NOT_FOUND;
+  }
   return base::File::FILE_OK;
 }
 
@@ -232,6 +231,9 @@
       GetLocalFilePath(context, dest_url, &dest_file_path);
   if (error != base::File::FILE_OK)
     return error;
+  if (IsHiddenItem(src_file_path)) {
+    return base::File::FILE_ERROR_NOT_FOUND;
+  }
   return NativeFileUtil::CopyOrMoveFile(
       src_file_path, dest_file_path, FileSystemOperation::CopyOrMoveOptionSet(),
       NativeFileUtil::CopyOrMoveModeForDestination(dest_url, true /* copy */));
diff --git a/storage/browser/file_system/local_file_util_unittest.cc b/storage/browser/file_system/local_file_util_unittest.cc
index 58c3247..e35865828 100644
--- a/storage/browser/file_system/local_file_util_unittest.cc
+++ b/storage/browser/file_system/local_file_util_unittest.cc
@@ -163,6 +163,124 @@
   ASSERT_FALSE(file.IsValid());
   EXPECT_EQ(base::File::FILE_ERROR_NOT_FOUND, file.error_details());
 }
+
+TEST_F(LocalFileUtilTest, EnsureFileExistsFailForSymlink) {
+  const char* target_name = "symlink_target";
+  base::File target_file = CreateFile(target_name);
+  ASSERT_TRUE(target_file.IsValid());
+  ASSERT_TRUE(target_file.created());
+  base::FilePath target_path = LocalPath(target_name);
+
+  const char* symlink_name = "symlink_file";
+  base::FilePath symlink_path = LocalPath(symlink_name);
+  ASSERT_TRUE(base::CreateSymbolicLink(target_path, symlink_path));
+  ASSERT_TRUE(FileExists(symlink_name));
+
+  bool created = false;
+  EXPECT_EQ(base::File::FILE_ERROR_NOT_FOUND,
+            EnsureFileExists(symlink_name, &created));
+  EXPECT_FALSE(created);
+}
+
+TEST_F(LocalFileUtilTest, TouchFailForSymlink) {
+  const char* target_name = "symlink_target";
+  base::File target_file = CreateFile(target_name);
+  ASSERT_TRUE(target_file.IsValid());
+  ASSERT_TRUE(target_file.created());
+  base::FilePath target_path = LocalPath(target_name);
+
+  const char* symlink_name = "symlink_file";
+  base::FilePath symlink_path = LocalPath(symlink_name);
+  ASSERT_TRUE(base::CreateSymbolicLink(target_path, symlink_path));
+  ASSERT_TRUE(FileExists(symlink_name));
+
+  std::unique_ptr<FileSystemOperationContext> context(NewContext());
+  EXPECT_EQ(base::File::FILE_ERROR_NOT_FOUND,
+            file_util()->Touch(context.get(), CreateURL(symlink_name),
+                               base::Time::Now(), base::Time::Now()));
+}
+
+TEST_F(LocalFileUtilTest, TruncateFailForSymlink) {
+  const char* target_name = "symlink_target";
+  base::File target_file = CreateFile(target_name);
+  ASSERT_TRUE(target_file.IsValid());
+  ASSERT_TRUE(target_file.created());
+  base::FilePath target_path = LocalPath(target_name);
+
+  const char* symlink_name = "symlink_file";
+  base::FilePath symlink_path = LocalPath(symlink_name);
+  ASSERT_TRUE(base::CreateSymbolicLink(target_path, symlink_path));
+  ASSERT_TRUE(FileExists(symlink_name));
+
+  std::unique_ptr<FileSystemOperationContext> context(NewContext());
+  EXPECT_EQ(base::File::FILE_ERROR_NOT_FOUND,
+            file_util()->Truncate(context.get(), CreateURL(symlink_name), 1));
+  EXPECT_EQ(0, GetSize(target_name));
+}
+
+TEST_F(LocalFileUtilTest, CopyOrMoveFileFailForSymlink) {
+  const char* target_name = "symlink_target";
+  base::File target_file = CreateFile(target_name);
+  ASSERT_TRUE(target_file.IsValid());
+  ASSERT_TRUE(target_file.created());
+  base::FilePath target_path = LocalPath(target_name);
+
+  const char* symlink_name = "symlink_file";
+  base::FilePath symlink_path = LocalPath(symlink_name);
+  ASSERT_TRUE(base::CreateSymbolicLink(target_path, symlink_path));
+  ASSERT_TRUE(FileExists(symlink_name));
+
+  const char* other_name = "other_file";
+  bool created;
+  ASSERT_EQ(base::File::FILE_OK, EnsureFileExists(other_name, &created));
+  ASSERT_TRUE(created);
+
+  std::unique_ptr<FileSystemOperationContext> context;
+  context = NewContext();
+  ASSERT_EQ(base::File::FILE_OK,
+            file_util()->Truncate(context.get(), CreateURL(other_name), 1020));
+
+  // Copying onto a symlink should fail.
+  context = NewContext();
+  EXPECT_EQ(base::File::FILE_ERROR_NOT_FOUND,
+            file_util()->CopyOrMoveFile(
+                context.get(), CreateURL(other_name), CreateURL(symlink_name),
+                FileSystemFileUtil::CopyOrMoveOptionSet(), true /* copy */));
+  EXPECT_EQ(0, GetSize(target_name));
+
+  // Copying from a symlink should fail.
+  context = NewContext();
+  EXPECT_EQ(base::File::FILE_ERROR_NOT_FOUND,
+            file_util()->CopyOrMoveFile(
+                context.get(), CreateURL(symlink_name), CreateURL(other_name),
+                FileSystemFileUtil::CopyOrMoveOptionSet(), true /* copy */));
+  EXPECT_EQ(1020, GetSize(other_name));
+}
+
+TEST_F(LocalFileUtilTest, CopyInForeignFileFailForSymlink) {
+  const char* target_name = "symlink_target";
+  base::File target_file = CreateFile(target_name);
+  ASSERT_TRUE(target_file.IsValid());
+  ASSERT_TRUE(target_file.created());
+  base::FilePath target_path = LocalPath(target_name);
+
+  const char* symlink_name = "symlink_file";
+  base::FilePath symlink_path = LocalPath(symlink_name);
+  ASSERT_TRUE(base::CreateSymbolicLink(target_path, symlink_path));
+  ASSERT_TRUE(FileExists(symlink_name));
+
+  base::ScopedTempDir foreign_dir;
+  ASSERT_TRUE(foreign_dir.CreateUniqueTempDir());
+  base::FilePath foreign_path =
+      foreign_dir.GetPath().AppendASCII("foreign_file");
+  ASSERT_TRUE(base::WriteFile(foreign_path, "data"));
+
+  std::unique_ptr<FileSystemOperationContext> context(NewContext());
+  EXPECT_EQ(base::File::FILE_ERROR_NOT_FOUND,
+            file_util()->CopyInForeignFile(context.get(), foreign_path,
+                                           CreateURL(symlink_name)));
+  EXPECT_EQ(0, GetSize(target_name));
+}
 #endif
 
 TEST_F(LocalFileUtilTest, EnsureFileExists) {
diff --git a/storage/browser/test/test_file_system_backend.cc b/storage/browser/test/test_file_system_backend.cc
index 97d0171..eab02c9 100644
--- a/storage/browser/test/test_file_system_backend.cc
+++ b/storage/browser/test/test_file_system_backend.cc
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/storage/browser/file_system/local_file_util_unittest.cc b/storage/browser/file_system/local_file_util_unittest.cc
index 58c3247..e35865828 100644
--- a/storage/browser/file_system/local_file_util_unittest.cc
+++ b/storage/browser/file_system/local_file_util_unittest.cc
@@ -163,6 +163,124 @@
   ASSERT_FALSE(file.IsValid());
   EXPECT_EQ(base::File::FILE_ERROR_NOT_FOUND, file.error_details());
 }
+
+TEST_F(LocalFileUtilTest, EnsureFileExistsFailForSymlink) {
+  const char* target_name = "symlink_target";
+  base::File target_file = CreateFile(target_name);
+  ASSERT_TRUE(target_file.IsValid());
+  ASSERT_TRUE(target_file.created());
+  base::FilePath target_path = LocalPath(target_name);
+
+  const char* symlink_name = "symlink_file";
+  base::FilePath symlink_path = LocalPath(symlink_name);
+  ASSERT_TRUE(base::CreateSymbolicLink(target_path, symlink_path));
+  ASSERT_TRUE(FileExists(symlink_name));
+
+  bool created = false;
+  EXPECT_EQ(base::File::FILE_ERROR_NOT_FOUND,
+            EnsureFileExists(symlink_name, &created));
+  EXPECT_FALSE(created);
+}
+
+TEST_F(LocalFileUtilTest, TouchFailForSymlink) {
+  const char* target_name = "symlink_target";
+  base::File target_file = CreateFile(target_name);
+  ASSERT_TRUE(target_file.IsValid());
+  ASSERT_TRUE(target_file.created());
+  base::FilePath target_path = LocalPath(target_name);
+
+  const char* symlink_name = "symlink_file";
+  base::FilePath symlink_path = LocalPath(symlink_name);
+  ASSERT_TRUE(base::CreateSymbolicLink(target_path, symlink_path));
+  ASSERT_TRUE(FileExists(symlink_name));
+
+  std::unique_ptr<FileSystemOperationContext> context(NewContext());
+  EXPECT_EQ(base::File::FILE_ERROR_NOT_FOUND,
+            file_util()->Touch(context.get(), CreateURL(symlink_name),
+                               base::Time::Now(), base::Time::Now()));
+}
+
+TEST_F(LocalFileUtilTest, TruncateFailForSymlink) {
+  const char* target_name = "symlink_target";
+  base::File target_file = CreateFile(target_name);
+  ASSERT_TRUE(target_file.IsValid());
+  ASSERT_TRUE(target_file.created());
+  base::FilePath target_path = LocalPath(target_name);
+
+  const char* symlink_name = "symlink_file";
+  base::FilePath symlink_path = LocalPath(symlink_name);
+  ASSERT_TRUE(base::CreateSymbolicLink(target_path, symlink_path));
+  ASSERT_TRUE(FileExists(symlink_name));
+
+  std::unique_ptr<FileSystemOperationContext> context(NewContext());
+  EXPECT_EQ(base::File::FILE_ERROR_NOT_FOUND,
+            file_util()->Truncate(context.get(), CreateURL(symlink_name), 1));
+  EXPECT_EQ(0, GetSize(target_name));
+}
+
+TEST_F(LocalFileUtilTest, CopyOrMoveFileFailForSymlink) {
+  const char* target_name = "symlink_target";
+  base::File target_file = CreateFile(target_name);
+  ASSERT_TRUE(target_file.IsValid());
+  ASSERT_TRUE(target_file.created());
+  base::FilePath target_path = LocalPath(target_name);
+
+  const char* symlink_name = "symlink_file";
+  base::FilePath symlink_path = LocalPath(symlink_name);
+  ASSERT_TRUE(base::CreateSymbolicLink(target_path, symlink_path));
+  ASSERT_TRUE(FileExists(symlink_name));
+
+  const char* other_name = "other_file";
+  bool created;
+  ASSERT_EQ(base::File::FILE_OK, EnsureFileExists(other_name, &created));
+  ASSERT_TRUE(created);
+
+  std::unique_ptr<FileSystemOperationContext> context;
+  context = NewContext();
+  ASSERT_EQ(base::File::FILE_OK,
+            file_util()->Truncate(context.get(), CreateURL(other_name), 1020));
+
+  // Copying onto a symlink should fail.
+  context = NewContext();
+  EXPECT_EQ(base::File::FILE_ERROR_NOT_FOUND,
+            file_util()->CopyOrMoveFile(
+                context.get(), CreateURL(other_name), CreateURL(symlink_name),
+                FileSystemFileUtil::CopyOrMoveOptionSet(), true /* copy */));
+  EXPECT_EQ(0, GetSize(target_name));
+
+  // Copying from a symlink should fail.
+  context = NewContext();
+  EXPECT_EQ(base::File::FILE_ERROR_NOT_FOUND,
+            file_util()->CopyOrMoveFile(
+                context.get(), CreateURL(symlink_name), CreateURL(other_name),
+                FileSystemFileUtil::CopyOrMoveOptionSet(), true /* copy */));
+  EXPECT_EQ(1020, GetSize(other_name));
+}
+
+TEST_F(LocalFileUtilTest, CopyInForeignFileFailForSymlink) {
+  const char* target_name = "symlink_target";
+  base::File target_file = CreateFile(target_name);
+  ASSERT_TRUE(target_file.IsValid());
+  ASSERT_TRUE(target_file.created());
+  base::FilePath target_path = LocalPath(target_name);
+
+  const char* symlink_name = "symlink_file";
+  base::FilePath symlink_path = LocalPath(symlink_name);
+  ASSERT_TRUE(base::CreateSymbolicLink(target_path, symlink_path));
+  ASSERT_TRUE(FileExists(symlink_name));
+
+  base::ScopedTempDir foreign_dir;
+  ASSERT_TRUE(foreign_dir.CreateUniqueTempDir());
+  base::FilePath foreign_path =
+      foreign_dir.GetPath().AppendASCII("foreign_file");
+  ASSERT_TRUE(base::WriteFile(foreign_path, "data"));
+
+  std::unique_ptr<FileSystemOperationContext> context(NewContext());
+  EXPECT_EQ(base::File::FILE_ERROR_NOT_FOUND,
+            file_util()->CopyInForeignFile(context.get(), foreign_path,
+                                           CreateURL(symlink_name)));
+  EXPECT_EQ(0, GetSize(target_name));
+}
 #endif
 
 TEST_F(LocalFileUtilTest, EnsureFileExists) {
diff --git a/storage/browser/test/test_file_system_backend.cc b/storage/browser/test/test_file_system_backend.cc
index 97d0171..eab02c9 100644
--- a/storage/browser/test/test_file_system_backend.cc
+++ b/storage/browser/test/test_file_system_backend.cc
@@ -47,6 +47,9 @@
                                      const FileSystemURL& file_system_url,
                                      base::FilePath* local_file_path) override {
     *local_file_path = base_path_.Append(file_system_url.path());
+    if (IsHiddenItem(*local_file_path)) {
+      return base::File::FILE_ERROR_NOT_FOUND;
+    }
     return base::File::FILE_OK;
   }
Loading diff…

Original Bug Report

reported by [email protected]

Arbitrary file overwrite via symlink bypass in LocalFileUtil

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 without the security team.

Overview: LocalFileUtil fails to validate symbolic links in operations like EnsureFileExists and Truncate. By executing a stream copy onto a symlink, a malicious app can potentially overwrite arbitrary host files. This bypasses the sandbox and could lead to Remote Code Execution.

Affected files:

  • storage/browser/file_system/local_file_util.cc
  • storage/browser/file_system/local_file_stream_writer.cc
  • storage/browser/file_system/native_file_util.cc

Estimated timestamp from git blame: 2024-08-07

Verdict: Potential arbitrary file overwrite leading to Sandbox Escape. Suggested Fix: Apply the IsHiddenItem() validation to EnsureFileExists, Truncate, CopyOrMoveFile, and CopyInForeignFile within storage/browser/file_system/local_file_util.cc. Additionally, enforce O_NOFOLLOW equivalents during file initialization in LocalFileStreamWriter.

Technical Details

Initial logic and routing parameters in the FileSystem API are validated; operations correctly resolve through IsolatedFileSystemBackend down to LocalFileUtil. While methods like GetFileInfo properly invoke IsHiddenItem() to block symlinks in sandboxed contexts, the remaining execution path completely bypasses this boundary.

During a stream copy operation (StreamCopyOrMoveImpl) onto an existing symlink, the system jumps directly to executing EnsureFileExists followed by Truncate. These functions instantiate base::File using base::File::FLAG_OPEN | base::File::FLAG_WRITE. Because this directly translates to standard POSIX open calls without O_NOFOLLOW, the underlying target file is silently followed, truncated, and directly overwritten with the stream’s payload by the browser process.

Suggested Reproduction Steps

(Note: These are potential steps; our tooling cannot run code to verify a working proof of concept.)

  1. Standard processing applied: The user extracts and selects an attacker-provided directory containing a symlink (e.g., pointing to ~/.bashrc) and a malicious payload file.
  2. The attacker’s web or Platform App invokes the copyTo() API to copy the payload over the symlink.
  3. The browser process natively follows the symlink without validation, arbitrarily overwriting the sensitive host file.

Evaluated with Chrome root at commit: f200f57a19490707ff8bc7aa5de3cbc443a3afad


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