Medium watch CVSS 9.6 chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
9.6
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactIncorrect reference resolution in FileSystem
DescriptionIncorrect reference resolution in FileSystem
ComponentFileSystem
Sub-componentStorage
Bug ClassLogic Error
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.
Tracker500038021
Fix commit835fc99abf93 (chromium/src) +127/-4
CWECWE-706
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-04-06 (per vendor advisory)
Fix landed2026-07-08 (public git)
Disclosed2026-08-25
Patch gap48 days — fix public in source before the advisory

Background

`LocalFileUtil`
the storage::FileSystemFileUtil implementation in storage/browser/file_system/local_file_util.cc. It maps a FileSystemURL to a real native path and runs file operations (CreateOrOpen, GetFileInfo, EnsureFileExists, Touch, Truncate, CopyOrMoveFile, CopyInForeignFile) through NativeFileUtil in the browser process.
`IsHiddenItem()`
a helper that, per the commit message, reports whether a path is a symbolic link. LocalFileUtil uses it to deliberately hide symlinks, reporting them as base::File::FILE_ERROR_NOT_FOUND.
`GetLocalFilePath()`
the virtual resolver that turns a FileSystemURL into a native base::FilePath. Every URL-based operation in the class calls it before touching the disk, and subclasses (such as the test backend in test_file_system_backend.cc) can override it.
Symlink following
native file APIs follow symbolic links by default. An operation on a link path therefore acts on the link’s target, which can be outside the directory the caller was granted.

Root Cause Analysis

By design, LocalFileUtil does not expose symlinks, but before the patch the IsHiddenItem() check sat only in CreateOrOpen() and GetFileInfo() (the removed lines) and in the enumerator. As the commit message says, EnsureFileExists(), Touch(), Truncate(), CopyOrMoveFile() and CopyInForeignFile() resolved the URL and passed the native path straight to NativeFileUtil without the check. Those write operations would therefore follow a symlink inside the exposed directory and create, change timestamps on, truncate or overwrite whatever file the link pointed to. The regression tests show this: before the fix, Truncate, a copy onto a symlink, and CopyInForeignFile onto a symlink would change symlink_target.

The fix moves the check into the shared resolver: right after *local_file_path = url.path(); the resolver now returns FILE_ERROR_NOT_FOUND when IsHiddenItem(*local_file_path) is true. Every URL-based operation, for both source and destination URLs, therefore rejects symlinks, and the now-redundant per-function checks in CreateOrOpen() / GetFileInfo() are removed. In one function, identified only by its body (it resolves only dest_url and always copies with true /* copy */, which matches CopyInForeignFile; the hunk does not show the name), the patch also adds an IsHiddenItem(src_file_path) check on the native source path before calling NativeFileUtil::CopyOrMoveFile. This is a tightening overall. One side effect: subclasses that override GetLocalFilePath() must now do the check themselves, which is why test_file_system_backend.cc’s override was updated too.

Key insight
The “symlinks are invisible” rule was enforced in a few operations rather than at the single place where URLs become native paths, so several write operations followed attacker-placed links. The fix enforces IsHiddenItem() inside path resolution, which covers every operation at once.

Attack Path

  1. Symlink in an exposed directory A directory exposed to a client through a LocalFileUtil-backed file system contains a symbolic link pointing at a file outside the intended scope. How the link gets there is not shown in the patch (inferred: for example extracted content, or a link placed by another local actor).
  2. Client gets write access A client with write access to that file system, such as an app or extension with file-system write permission or a platform file manager (inferred; the patch does not name the frontend), issues Truncate, Touch, EnsureFileExists or a copy targeting the link’s FileSystemURL.
  3. Resolution without symlink check Before the fix, path resolution returned the link path with FILE_OK, and these operations passed it to NativeFileUtil, which follows the link.
  4. Out-of-scope file modified The link target is truncated, has its timestamps changed, or is overwritten by copied data, as the tests’ GetSize(target_name) assertions encode. Copying from a symlink source could likewise read data from outside the scope into an accessible file (inferred from the “copying from a symlink should fail” test).

Impact Assessment

The bug lets a client of a LocalFileUtil-backed file system make the browser process read or write files outside the granted directory by following symlinks. This is a confused-deputy file-integrity issue, with a possible confidentiality issue through copy-from-symlink (inferred). It needs a symlink already present inside an exposed directory and a caller allowed to run write operations there. The patch does not show ordinary web content reaching these write paths, so web reachability is not established (inferred). The vendor advisory rates this medium severity and does not report in-the-wild exploitation; NVD scores it CVSS 9.6.

Changed Functions

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

Files Changed

  • storage/browser/test/test_file_system_backend.cc
  • storage/browser/file_system/local_file_util_unittest.cc
  • storage/browser/file_system/local_file_util.cc

Audit Directions

  • Overrides of `GetLocalFilePath()`
    Because the checks were removed from CreateOrOpen() and GetFileInfo() and centralized in the resolver, any production subclass of LocalFileUtil that overrides GetLocalFilePath() without calling IsHiddenItem() would now let symlinks through even on those previously protected operations. Audit every override.
  • Check-then-use races on native paths
    IsHiddenItem() checks the path, and the NativeFileUtil call then reopens it by name. Look for time-of-check/time-of-use windows where a regular file can be swapped for a symlink between the two, and for intermediate path components that are links (inferred residual risk).
  • Other path-mapping file utils
    Review other FileSystemFileUtil / AsyncFileUtil implementations and backends (isolated, native, external or mounted, File System Access) for operations that resolve a URL to a native path and then follow links with default base::File semantics.
From 835fc99abf93cf8213729a7801604f284f5fa709 Mon Sep 17 00:00:00 2001
From: Eriko Kurimoto <elkurin@google.com>
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 <leimy@chromium.org>
Commit-Queue: Mingyu Lei <leimy@chromium.org>
Auto-Submit: Eriko Kurimoto <elkurin@chromium.org>
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
@@ -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…

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 vm...@google.com

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