Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactIncorrect authorization in FileSystem
DescriptionIncorrect authorization in FileSystem
ComponentFileSystem
Bug ClassLogic Error
Tracker514454739
Fix commitf7317c7f70b7 (chromium/src) +64/-67
CISA KEVNot listed
CreditedGoogle
Disclosed2026-08-25

Changed Functions

FunctionChangeNotes
if
chrome/browser/file_system_access/BUILD.gn
modified
TEST_F
chrome/browser/file_system_access/chrome_file_system_access_permission_context_unittest.cc
modified

Files Changed

  • android_webview/browser/file_system_access/aw_file_system_access_permission_context.cc
  • android_webview/browser/file_system_access/aw_file_system_access_permission_context.h
  • chrome/browser/file_system_access/BUILD.gn
  • chrome/browser/file_system_access/chrome_file_system_access_permission_context.cc
  • chrome/browser/file_system_access/chrome_file_system_access_permission_context.h
  • chrome/browser/file_system_access/chrome_file_system_access_permission_context_unittest.cc
  • content/browser/file_system_access/file_system_access_directory_handle_impl.cc
  • content/browser/file_system_access/file_system_access_directory_handle_impl_unittest.cc
  • content/browser/file_system_access/file_system_access_file_handle_impl_unittest.cc
From f7317c7f70b7ed9650f993e4169e6d3f30c9a578 Mon Sep 17 00:00:00 2001
From: Eriko Kurimoto <[email protected]>
Date: Tue, 07 Jul 2026 17:19:42 -0700
Subject: [PATCH] FSA: Look up file type danger level without download overrides

ChromeFileSystemAccessPermissionContext passes the requesting origin's
URL as the "source URL" when querying FileTypePolicies for a file type's
danger level. FileTypePolicies treats that argument as the URL a
download came from and applies download-specific overrides on top of the
configured danger level (for example, treating any file:// URL with no
host as NOT_DANGEROUS because "the file is already on disk").

These overrides are not applicable to the File System Access API, where
the danger level gates creation of new files via
getFileHandle()/move()/rename() and showSaveFilePicker(). Pass an empty
source URL and null prefs so the raw configured danger level is used
instead.

Bug: 514454739
Change-Id: I3733d88f8f88ecf5866605bed5b5e93821e43a63
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8033861
Commit-Queue: Eriko Kurimoto <[email protected]>
Auto-Submit: Eriko Kurimoto <[email protected]>
Reviewed-by: Peter Conn <[email protected]>
Reviewed-by: Rakina Zata Amni <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1658401}
---

diff --git a/android_webview/browser/file_system_access/aw_file_system_access_permission_context.cc b/android_webview/browser/file_system_access/aw_file_system_access_permission_context.cc
index e9b7890..e2af0de5 100644
--- a/android_webview/browser/file_system_access/aw_file_system_access_permission_context.cc
+++ b/android_webview/browser/file_system_access/aw_file_system_access_permission_context.cc
@@ -157,8 +157,7 @@
 }
 
 bool AwFileSystemAccessPermissionContext::IsFileTypeDangerous(
-    const base::FilePath& path,
-    const url::Origin& origin) {
+    const base::FilePath& path) {
   return false;
 }
 
diff --git a/android_webview/browser/file_system_access/aw_file_system_access_permission_context.h b/android_webview/browser/file_system_access/aw_file_system_access_permission_context.h
index 2bac1ac..0849163 100644
--- a/android_webview/browser/file_system_access/aw_file_system_access_permission_context.h
+++ b/android_webview/browser/file_system_access/aw_file_system_access_permission_context.h
@@ -58,8 +58,7 @@
       content::RenderFrameHost* rfh) override;
   bool CanObtainReadPermission(const url::Origin& origin) override;
   bool CanObtainWritePermission(const url::Origin& origin) override;
-  bool IsFileTypeDangerous(const base::FilePath& path,
-                           const url::Origin& origin) override;
+  bool IsFileTypeDangerous(const base::FilePath& path) override;
   void SetLastPickedDirectory(const url::Origin& origin,
                               const std::string& id,
                               const content::PathInfo& path_info) override;
diff --git a/chrome/browser/file_system_access/BUILD.gn b/chrome/browser/file_system_access/BUILD.gn
index d3b7987..3256b774 100644
--- a/chrome/browser/file_system_access/BUILD.gn
+++ b/chrome/browser/file_system_access/BUILD.gn
@@ -156,6 +156,10 @@
     deps += [ "//chrome/browser/enterprise/connectors/test:test_support" ]
   }
 
+  if (safe_browsing_mode != 0) {
+    deps += [ "//components/safe_browsing/content/common:file_type_policies_test_support" ]
+  }
+
   if (!is_android) {
     deps +=
         [ "//chrome/browser/web_applications:web_applications_test_support" ]
diff --git a/chrome/browser/file_system_access/chrome_file_system_access_permission_context.cc b/chrome/browser/file_system_access/chrome_file_system_access_permission_context.cc
index a739d4b0..f00cc5b 100644
--- a/chrome/browser/file_system_access/chrome_file_system_access_permission_context.cc
+++ b/chrome/browser/file_system_access/chrome_file_system_access_permission_context.cc
@@ -740,11 +740,12 @@
 
 #if BUILDFLAG(SAFE_BROWSING_AVAILABLE)
 safe_browsing::DownloadFileType::DangerLevel GetFileTypeDangerLevel(
-    const base::FilePath& path,
-    const url::Origin& origin,
-    Profile* profile) {
+    const base::FilePath& path) {
+  // Passing an empty source URL and null prefs ensures the result reflects
+  // only the configured danger level for the file type, without applying any
+  // download-specific overrides.
   return safe_browsing::FileTypePolicies::GetInstance()->GetFileDangerLevel(
-      path, origin.GetURL(), profile->GetPrefs());
+      path, GURL(), /*prefs=*/nullptr);
 }
 #endif
 
@@ -2037,11 +2038,9 @@
 }
 
 bool ChromeFileSystemAccessPermissionContext::IsFileTypeDangerous(
-    const base::FilePath& path,
-    const url::Origin& origin) {
+    const base::FilePath& path) {
 #if BUILDFLAG(SAFE_BROWSING_AVAILABLE)
-  return GetFileTypeDangerLevel(path, origin,
-                                Profile::FromBrowserContext(profile_)) ==
+  return GetFileTypeDangerLevel(path) ==
          safe_browsing::DownloadFileType::DANGEROUS;
 #else
   return false;
@@ -2343,8 +2342,7 @@
     // See https://crbug.com/40059513#comment5 for justification for why we show
     // the prompt if `danger_level` is ALLOW_ON_USER_GESTURE as well as
     // DANGEROUS.
-    auto danger_level = GetFileTypeDangerLevel(
-        path_info.path, origin, Profile::FromBrowserContext(profile_));
+    auto danger_level = GetFileTypeDangerLevel(path_info.path);
     if (danger_level == safe_browsing::DownloadFileType::DANGEROUS ||
         danger_level ==
             safe_browsing::DownloadFileType::ALLOW_ON_USER_GESTURE) {
diff --git a/chrome/browser/file_system_access/chrome_file_system_access_permission_context.h b/chrome/browser/file_system_access/chrome_file_system_access_permission_context.h
index 4111e2c..955504d 100644
--- a/chrome/browser/file_system_access/chrome_file_system_access_permission_context.h
+++ b/chrome/browser/file_system_access/chrome_file_system_access_permission_context.h
@@ -300,8 +300,7 @@
       std::unique_ptr<content::FileSystemAccessWriteItem> item,
       content::GlobalRenderFrameHostId frame_id,
       base::OnceCallback<void(AfterWriteCheckResult)> callback) override;
-  bool IsFileTypeDangerous(const base::FilePath& path,
-                           const url::Origin& origin) override;
+  bool IsFileTypeDangerous(const base::FilePath& path) override;
   base::expected<void, std::string> CanShowFilePicker(
       content::RenderFrameHost* rfh) override;
   bool CanObtainReadPermission(const url::Origin& origin) override;
diff --git a/chrome/browser/file_system_access/chrome_file_system_access_permission_context_unittest.cc b/chrome/browser/file_system_access/chrome_file_system_access_permission_context_unittest.cc
index 803f237..fce6443 100644
--- a/chrome/browser/file_system_access/chrome_file_system_access_permission_context_unittest.cc
+++ b/chrome/browser/file_system_access/chrome_file_system_access_permission_context_unittest.cc
@@ -42,6 +42,7 @@
 #include "components/permissions/permission_decision_auto_blocker.h"
 #include "components/permissions/permission_uma_util.h"
 #include "components/permissions/permission_util.h"
+#include "components/safe_browsing/buildflags.h"
 #include "components/sync_preferences/testing_pref_service_syncable.h"
 #include "content/public/browser/render_process_host.h"
 #include "content/public/browser/site_instance.h"
@@ -79,6 +80,10 @@
 #include "chrome/browser/policy/dm_token_utils.h"
 #endif
 
+#if BUILDFLAG(SAFE_BROWSING_AVAILABLE)
+#include "components/safe_browsing/content/common/file_type_policies_test_util.h"
+#endif
+
 using content::BrowserContext;
 using content::PathInfo;
 using content::PathType;
@@ -1422,6 +1427,16 @@
   EXPECT_TRUE(permission_context()->CanObtainWritePermission(kChromeOrigin));
 }
 
+#if BUILDFLAG(SAFE_BROWSING_AVAILABLE)
+TEST_F(ChromeFileSystemAccessPermissionContextTest, IsFileTypeDangerous) {
+  safe_browsing::FileTypePoliciesTestOverlay scoped_dangerous =
+      safe_browsing::ScopedMarkAllFilesDangerousForTesting();
+
+  const base::FilePath kPath(FILE_PATH_LITERAL("/foo/bar.dll"));
+  EXPECT_TRUE(permission_context()->IsFileTypeDangerous(kPath));
+}
+#endif  // BUILDFLAG(SAFE_BROWSING_AVAILABLE)
+
 TEST_F(ChromeFileSystemAccessPermissionContextTest, PolicyReadGuardPermission) {
   auto* prefs = profile()->GetTestingPrefService();
   prefs->SetManagedPref(prefs::kManagedDefaultFileSystemReadGuardSetting,
diff --git a/content/browser/file_system_access/file_system_access_directory_handle_impl.cc b/content/browser/file_system_access/file_system_access_directory_handle_impl.cc
index b6c388d5..17ee6ab 100644
--- a/content/browser/file_system_access/file_system_access_directory_handle_impl.cc
+++ b/content/browser/file_system_access/file_system_access_directory_handle_impl.cc
@@ -899,8 +899,7 @@
   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
 
   const storage::FileSystemURL& parent = url();
-  if (!manager()->IsSafePathComponent(
-          parent.type(), context().storage_key.origin(), basename)) {
+  if (!manager()->IsSafePathComponent(parent.type(), basename)) {
     return file_system_access_error::FromStatus(
         FileSystemAccessStatus::kInvalidArgument, "Name is not allowed.");
   }
diff --git a/content/browser/file_system_access/file_system_access_directory_handle_impl_unittest.cc b/content/browser/file_system_access/file_system_access_directory_handle_impl_unittest.cc
index 53c5314..1874eb8c 100644
--- a/content/browser/file_system_access/file_system_access_directory_handle_impl_unittest.cc
+++ b/content/browser/file_system_access/file_system_access_directory_handle_impl_unittest.cc
@@ -140,7 +140,7 @@
         FileSystemAccessManagerImpl::SharedHandleState(deny_grant_,
                                                        deny_grant_));
 
-    EXPECT_CALL(permission_context_, IsFileTypeDangerous_(_, _))
+    EXPECT_CALL(permission_context_, IsFileTypeDangerous_(_))
         .WillRepeatedly(testing::Return(false));
   }
 
diff --git a/content/browser/file_system_access/file_system_access_file_handle_impl_unittest.cc b/content/browser/file_system_access/file_system_access_file_handle_impl_unittest.cc
index 3fb4ddf5..0880dd8 100644
--- a/content/browser/file_system_access/file_system_access_file_handle_impl_unittest.cc
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/chrome/browser/file_system_access/chrome_file_system_access_permission_context_unittest.cc b/chrome/browser/file_system_access/chrome_file_system_access_permission_context_unittest.cc
index 803f237..fce6443 100644
--- a/chrome/browser/file_system_access/chrome_file_system_access_permission_context_unittest.cc
+++ b/chrome/browser/file_system_access/chrome_file_system_access_permission_context_unittest.cc
@@ -42,6 +42,7 @@
 #include "components/permissions/permission_decision_auto_blocker.h"
 #include "components/permissions/permission_uma_util.h"
 #include "components/permissions/permission_util.h"
+#include "components/safe_browsing/buildflags.h"
 #include "components/sync_preferences/testing_pref_service_syncable.h"
 #include "content/public/browser/render_process_host.h"
 #include "content/public/browser/site_instance.h"
@@ -79,6 +80,10 @@
 #include "chrome/browser/policy/dm_token_utils.h"
 #endif
 
+#if BUILDFLAG(SAFE_BROWSING_AVAILABLE)
+#include "components/safe_browsing/content/common/file_type_policies_test_util.h"
+#endif
+
 using content::BrowserContext;
 using content::PathInfo;
 using content::PathType;
@@ -1422,6 +1427,16 @@
   EXPECT_TRUE(permission_context()->CanObtainWritePermission(kChromeOrigin));
 }
 
+#if BUILDFLAG(SAFE_BROWSING_AVAILABLE)
+TEST_F(ChromeFileSystemAccessPermissionContextTest, IsFileTypeDangerous) {
+  safe_browsing::FileTypePoliciesTestOverlay scoped_dangerous =
+      safe_browsing::ScopedMarkAllFilesDangerousForTesting();
+
+  const base::FilePath kPath(FILE_PATH_LITERAL("/foo/bar.dll"));
+  EXPECT_TRUE(permission_context()->IsFileTypeDangerous(kPath));
+}
+#endif  // BUILDFLAG(SAFE_BROWSING_AVAILABLE)
+
 TEST_F(ChromeFileSystemAccessPermissionContextTest, PolicyReadGuardPermission) {
   auto* prefs = profile()->GetTestingPrefService();
   prefs->SetManagedPref(prefs::kManagedDefaultFileSystemReadGuardSetting,
diff --git a/content/browser/file_system_access/file_system_access_directory_handle_impl_unittest.cc b/content/browser/file_system_access/file_system_access_directory_handle_impl_unittest.cc
index 53c5314..1874eb8c 100644
--- a/content/browser/file_system_access/file_system_access_directory_handle_impl_unittest.cc
+++ b/content/browser/file_system_access/file_system_access_directory_handle_impl_unittest.cc
@@ -140,7 +140,7 @@
         FileSystemAccessManagerImpl::SharedHandleState(deny_grant_,
                                                        deny_grant_));
 
-    EXPECT_CALL(permission_context_, IsFileTypeDangerous_(_, _))
+    EXPECT_CALL(permission_context_, IsFileTypeDangerous_(_))
         .WillRepeatedly(testing::Return(false));
   }
diff --git a/content/browser/file_system_access/file_system_access_file_handle_impl_unittest.cc b/content/browser/file_system_access/file_system_access_file_handle_impl_unittest.cc
index 3fb4ddf5..0880dd8 100644
--- a/content/browser/file_system_access/file_system_access_file_handle_impl_unittest.cc
+++ b/content/browser/file_system_access/file_system_access_file_handle_impl_unittest.cc
@@ -1217,8 +1217,7 @@
     auto origin = test_src_storage_key_.origin();
     auto target_basename = target.BaseName();
 
-    EXPECT_CALL(permission_context_,
-                IsFileTypeDangerous_(target_basename, origin))
+    EXPECT_CALL(permission_context_, IsFileTypeDangerous_(target_basename))
         .WillRepeatedly(testing::Return(!expects_safe_name));
 
     if (expected_status == FileSystemAccessStatus::kOk) {
@@ -1336,8 +1335,7 @@
     auto origin = test_src_storage_key_.origin();
     auto target_basename = target.BaseName();
 
-    EXPECT_CALL(permission_context_,
-                IsFileTypeDangerous_(target_basename, origin))
+    EXPECT_CALL(permission_context_, IsFileTypeDangerous_(target_basename))
         .WillRepeatedly(testing::Return(false));
     EXPECT_CALL(
         permission_context_,
@@ -1400,8 +1398,7 @@
     auto origin = test_src_storage_key_.origin();
     auto target_basename = target.BaseName();
 
-    EXPECT_CALL(permission_context_,
-                IsFileTypeDangerous_(target_basename, origin))
+    EXPECT_CALL(permission_context_, IsFileTypeDangerous_(target_basename))
         .WillRepeatedly(testing::Return(!expects_safe_name));
     if (expected_sensitive_entry_result.has_value()) {
       EXPECT_CALL(
@@ -2081,8 +2078,7 @@
   auto source_handle =
       GetHandleWithPermissions(source, allow_grant_, allow_grant_);
   auto target_basename = target.BaseName();
-  EXPECT_CALL(permission_context_,
-              IsFileTypeDangerous_(target_basename, origin))
+  EXPECT_CALL(permission_context_, IsFileTypeDangerous_(target_basename))
       .WillOnce(testing::Return(false));
 
   // Mock the target file permission check. Returning `ask_grant_` (which is not
@@ -2148,8 +2144,7 @@
   auto source_handle =
       GetHandleWithPermissions(source, allow_grant_, allow_grant_);
   auto target_basename = target.BaseName();
-  EXPECT_CALL(permission_context_,
-              IsFileTypeDangerous_(target_basename, origin))
+  EXPECT_CALL(permission_context_, IsFileTypeDangerous_(target_basename))
       .WillOnce(testing::Return(false));
   // Since target is outside homedir, it should fallback to checking target file
   // grants (legacy behavior).
diff --git a/content/browser/file_system_access/file_system_access_manager_impl_unittest.cc b/content/browser/file_system_access/file_system_access_manager_impl_unittest.cc
index d655ef7b..e81f04bb 100644
--- a/content/browser/file_system_access/file_system_access_manager_impl_unittest.cc
+++ b/content/browser/file_system_access/file_system_access_manager_impl_unittest.cc
@@ -2575,36 +2575,36 @@
   };
 
   for (const char* component : kSafePathComponents) {
-    EXPECT_TRUE(manager_->IsSafePathComponent(
-        storage::kFileSystemTypeTemporary, kTestStorageKey.origin(), component))
+    EXPECT_TRUE(manager_->IsSafePathComponent(storage::kFileSystemTypeTemporary,
+                                              component))
         << component;
-    EXPECT_TRUE(manager_->IsSafePathComponent(
-        storage::kFileSystemTypeLocal, kTestStorageKey.origin(), component))
+    EXPECT_TRUE(
+        manager_->IsSafePathComponent(storage::kFileSystemTypeLocal, component))
         << component;
-    EXPECT_TRUE(manager_->IsSafePathComponent(
-        storage::kFileSystemTypeExternal, kTestStorageKey.origin(), component))
+    EXPECT_TRUE(manager_->IsSafePathComponent(storage::kFileSystemTypeExternal,
+                                              component))
         << component;
   }
   for (const char* component : kAlwaysUnsafePathComponents) {
     EXPECT_FALSE(manager_->IsSafePathComponent(
-        storage::kFileSystemTypeTemporary, kTestStorageKey.origin(), component))
+        storage::kFileSystemTypeTemporary, component))
         << component;
-    EXPECT_FALSE(manager_->IsSafePathComponent(
-        storage::kFileSystemTypeLocal, kTestStorageKey.origin(), component))
+    EXPECT_FALSE(
+        manager_->IsSafePathComponent(storage::kFileSystemTypeLocal, component))
         << component;
-    EXPECT_FALSE(manager_->IsSafePathComponent(
-        storage::kFileSystemTypeExternal, kTestStorageKey.origin(), component))
+    EXPECT_FALSE(manager_->IsSafePathComponent(storage::kFileSystemTypeExternal,
+                                               component))
         << component;
   }
   for (const char* component : kUnsafeLocalPathComponents) {
-    EXPECT_TRUE(manager_->IsSafePathComponent(
-        storage::kFileSystemTypeTemporary, kTestStorageKey.origin(), component))
+    EXPECT_TRUE(manager_->IsSafePathComponent(storage::kFileSystemTypeTemporary,
+                                              component))
         << component;
-    EXPECT_FALSE(manager_->IsSafePathComponent(
-        storage::kFileSystemTypeLocal, kTestStorageKey.origin(), component))
+    EXPECT_FALSE(
+        manager_->IsSafePathComponent(storage::kFileSystemTypeLocal, component))
         << component;
-    EXPECT_FALSE(manager_->IsSafePathComponent(
-        storage::kFileSystemTypeExternal, kTestStorageKey.origin(), component))
+    EXPECT_FALSE(manager_->IsSafePathComponent(storage::kFileSystemTypeExternal,
+                                               component))
         << component;
   }
 }
diff --git a/content/public/test/fake_file_system_access_permission_context.cc b/content/public/test/fake_file_system_access_permission_context.cc
index cc166c1..ea9942ec 100644
--- a/content/public/test/fake_file_system_access_permission_context.cc
+++ b/content/public/test/fake_file_system_access_permission_context.cc
@@ -53,8 +53,7 @@
 }
 
 bool FakeFileSystemAccessPermissionContext::IsFileTypeDangerous(
-    const base::FilePath& path,
-    const url::Origin& origin) {
+    const base::FilePath& path) {
   return false;
 }
diff --git a/content/public/test/fake_file_system_access_permission_context.h b/content/public/test/fake_file_system_access_permission_context.h
index 9e3c0cb..e81f21e 100644
--- a/content/public/test/fake_file_system_access_permission_context.h
+++ b/content/public/test/fake_file_system_access_permission_context.h
@@ -50,8 +50,7 @@
       GlobalRenderFrameHostId frame_id,
       base::OnceCallback<void(AfterWriteCheckResult)> callback) override;
 
-  bool IsFileTypeDangerous(const base::FilePath& path,
-                           const url::Origin& origin) override;
+  bool IsFileTypeDangerous(const base::FilePath& path) override;
 
   base::expected<void, std::string> CanShowFilePicker(
       content::RenderFrameHost* rfh) override;
Loading diff…

Original Bug Report

reported by [email protected]

Potential Safe Browsing bypass in File System Access API via getFileHandle

Flapjack, 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 logic error in the File System Access API allows sites with directory write access to silently create executable files (like .exe) without triggering the Safe Browsing warning dialog. This occurs because getFileHandle incorrectly passes UserAction::kNone during sensitive entry checks.

Affected files:

  • content/browser/file_system_access/file_system_access_directory_handle_impl.cc
  • chrome/browser/file_system_access/chrome_file_system_access_permission_context.cc

Estimated timestamp from git blame: 2024-10-03

Description

A potential logic error exists in the File System Access API that bypasses the Safe Browsing ‘Dangerous File Type’ warning when creating new files via a directory handle.

When directoryHandle.getFileHandle(name, {create: true}) is called, the system performs a sensitive entry access check to ensure the file path is safe. However, in FileSystemAccessDirectoryHandleImpl::GetFileResolved, this check is initiated with UserAction::kNone instead of UserAction::kSave.

In ChromeFileSystemAccessPermissionContext::DidCheckPathAgainstBlocklist, an action level of kNone triggers an early return. This completely bypasses the subsequent logic designed to display the FileSystemAccessDangerousFileDialog for file extensions with a DangerLevel of ALLOW_ON_USER_GESTURE (which includes .exe, .bat, .sh, .jar, etc., on their respective platforms).

Because ALLOW_ON_USER_GESTURE files are not blocked by the earlier IsSafePathComponent check (which only blocks strictly DANGEROUS files like .lnk), a malicious site that has obtained a directory handle can silently drop executables without the standard user confirmation dialog.

Potential Reproduction Steps

(Note: These are suggested steps; our tooling has not executed a live PoC.)

  1. An attacker hosts a malicious website and tricks a user into granting write access to a local directory (e.g., their Downloads or Desktop folder) using window.showDirectoryPicker({mode: 'readwrite'}).
  2. The malicious site executes const fileHandle = await directoryHandle.getFileHandle("payload.exe", {create: true});.
  3. The browser processes this in FileSystemAccessDirectoryHandleImpl::GetFileResolved. It calls ConfirmSensitiveEntryAccess with UserAction::kNone.
  4. In DidCheckPathAgainstBlocklist, the user_action == UserAction::kNone check causes an early return, bypassing the ALLOW_ON_USER_GESTURE prompt for the .exe file.
  5. The site successfully gets the handle and can write malware into payload.exe using createWritable().
  6. The executable is dropped onto the user’s filesystem without the intended defense-in-depth warning.

Technical Details

In content/browser/file_system_access/file_system_access_directory_handle_impl.cc (GetFileResolved):

    manager()->permission_context()->ConfirmSensitiveEntryAccess(
        context().storage_key.origin(), path_info, HandleType::kFile,
        UserAction::kNone, context().frame_id, // <-- Flaw: kNone used instead of kSave
        base::BindOnce(&FileSystemAccessDirectoryHandleImpl::DoGetFile, ...));

In chrome/browser/file_system_access/chrome_file_system_access_permission_context.cc (DidCheckPathAgainstBlocklist):

  if (user_action == UserAction::kNone) {
    std::move(callback).Run(should_block ? SensitiveEntryResult::kAbort
                                         : SensitiveEntryResult::kAllowed);
    return; // <-- Early return bypasses the dialog below
  }

  // ... later in the function ...

#if BUILDFLAG(SAFE_BROWSING_AVAILABLE)
  // If attempting to save a file with a dangerous extension, prompt the user...
  if (handle_type == HandleType::kFile && user_action == UserAction::kSave) {
      // Shows FileSystemAccessDangerousFileDialog if ALLOW_ON_USER_GESTURE

Suggested Fix

Update FileSystemAccessDirectoryHandleImpl::GetFileResolved to pass UserAction::kSave instead of UserAction::kNone when create is true. This aligns the behavior with other API surfaces like move(), ensuring the Safe Browsing warning is properly displayed when dangerous files are created.

Evaluated with Chrome root at commit: b7d0c4d810da1b31400f198c70d9720fc8f0e5a0


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