Medium watch CVSS 4.3 chrome Authorization Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
4.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactIncorrect authorization in FileSystem
DescriptionIncorrect authorization in FileSystem
ComponentFileSystem
Sub-componentFSA
Bug ClassAuthorization 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.
Tracker514454739
Fix commitf7317c7f70b7 (chromium/src) +64/-67
CWECWE-863
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:L/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-05-19 (per vendor advisory)
Fix landed2026-07-08 (public git)
Disclosed2026-08-25
Patch gap48 days — fix public in source before the advisory

Background

File System Access API
web API (getFileHandle(), move(), rename(), showSaveFilePicker()) letting a site create and manipulate files on disk, gated by permission and dangerous-file-type checks.
`FileTypePolicies::GetFileDangerLevel`
Safe Browsing service that returns a file extension’s danger level, applying download-specific overrides based on the source URL and profile prefs.
Danger-level overrides
special cases inside GetFileDangerLevel such as treating a hostless file:// source URL as NOT_DANGEROUS because “the file is already on disk.”
`IsSafePathComponent`
content-layer check (in file_system_access_manager_impl.cc) that rejects a file name if IsFileTypeDangerous reports its type dangerous, blocking creation/rename to that name.

Root Cause Analysis

ChromeFileSystemAccessPermissionContext’s GetFileTypeDangerLevel passed the requesting origin’s URL as the “source URL” (origin.GetURL()) and the profile prefs into FileTypePolicies::GetFileDangerLevel. GetFileDangerLevel interprets that source URL as where a download originated and layers download-specific overrides on top of the configured danger level — for example downgrading files whose source is a hostless file:// URL to NOT_DANGEROUS. These overrides are semantically wrong for the File System Access API, where the “danger level” instead gates creation of new files, so certain origins/URLs could cause a genuinely dangerous file type to be reported as safe, letting IsSafePathComponent permit creating or renaming to a dangerous extension.

The fix passes an empty GURL() source and nullptr prefs so GetFileDangerLevel returns only the raw configured danger level, and threads the change through by dropping the now-unused origin parameter from IsFileTypeDangerous, GetFileTypeDangerLevel, and IsSafePathComponent.

Key insight
The single mistake was feeding the requesting origin’s URL and prefs into a download-oriented danger-level lookup, so download-only overrides suppressed the dangerous classification for File System Access operations; the fix queries the raw configured danger level with an empty source URL and null prefs.

Attack Path

  1. Obtain File System Access grant A site (or WebView context) with write permission uses getFileHandle({create:true}), move(), rename(), or showSaveFilePicker() to attempt creating a file with a dangerous extension (e.g. .dll).
  2. Reach the danger check The operation calls IsSafePathComponent, which invokes permission_context_->IsFileTypeDangerous(component, origin) and thence GetFileTypeDangerLevel with the origin’s URL as the source.
  3. Trigger a download override Because the origin-derived source URL activates a download-specific override (such as the hostless file:// “already on disk” downgrade), GetFileDangerLevel returns a non-DANGEROUS level for the dangerous type. (inferred)
  4. Create the dangerous file IsFileTypeDangerous therefore returns false, IsSafePathComponent accepts the name, and the site creates/renames to a dangerous file type that policy should have blocked.

Impact Assessment

A web origin using the File System Access API could get a dangerous file type mis-classified as safe and thereby create or rename files to dangerous extensions that Safe Browsing policy intends to block — an incorrect-authorization issue in the browser process as the advisory describes. Preconditions include an existing File System Access write grant and an origin/URL that activates a download-specific override. The vendor advisory rates this medium severity and does not report in-the-wild exploitation; NVD scores it CVSS 4.3. The advisory reports no exploited-in-the-wild status or severity; further consequences are (inferred).

Changed Functions

FunctionChangeNotes
TEST_F
chrome/browser/file_system_access/chrome_file_system_access_permission_context_unittest.cc
modified
if
content/browser/file_system_access/file_system_access_file_handle_impl_unittest.cc
modified
for
content/browser/file_system_access/file_system_access_manager_impl_unittest.cc
modified

Files Changed

  • chrome/browser/file_system_access/chrome_file_system_access_permission_context_unittest.cc
  • chrome/browser/file_system_access/chrome_file_system_access_permission_context.h
  • content/browser/file_system_access/mock_file_system_access_permission_context.h
  • content/public/browser/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
  • content/public/test/fake_file_system_access_permission_context.h
  • content/browser/file_system_access/file_system_access_directory_handle_impl.cc
  • content/browser/file_system_access/file_system_access_manager_impl.cc
  • content/browser/file_system_access/file_system_access_manager_impl_unittest.cc
  • android_webview/browser/file_system_access/aw_file_system_access_permission_context.cc
  • content/browser/file_system_access/mock_file_system_access_permission_context.cc
  • android_webview/browser/file_system_access/aw_file_system_access_permission_context.h
  • content/browser/file_system_access/file_system_access_file_handle_impl_unittest.cc
  • content/public/test/fake_file_system_access_permission_context.cc
  • content/browser/file_system_access/file_system_access_manager_impl.h
  • content/browser/file_system_access/file_system_access_handle_base.cc
  • content/browser/file_system_access/file_system_access_directory_handle_impl_unittest.cc

Audit Directions

  • Repurposed policy queries
    Audit places that reuse a download/Safe-Browsing API for a non-download decision (file creation, extraction, install) and pass a contextual URL/prefs that silently enable inapplicable overrides.
  • Source-URL-dependent classification
    Look for GetFileDangerLevel-style calls where the “source URL” is attacker-influenced (origin, file://, empty host) and can downgrade danger levels.
  • Fail-open danger checks
    Review dangerous-type gates that default to non-dangerous when inputs are unusual; ensure the gate reflects the operation’s real threat model rather than the download one.
From f7317c7f70b7ed9650f993e4169e6d3f30c9a578 Mon Sep 17 00:00:00 2001
From: Eriko Kurimoto <elkurin@google.com>
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 <elkurin@chromium.org>
Auto-Submit: Eriko Kurimoto <elkurin@chromium.org>
Reviewed-by: Peter Conn <peconn@chromium.org>
Reviewed-by: Rakina Zata Amni <rakina@chromium.org>
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/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/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_handle_base.cc b/content/browser/file_system_access/file_system_access_handle_base.cc
index 2571f18..b004cbc 100644
--- a/content/browser/file_system_access/file_system_access_handle_base.cc
+++ b/content/browser/file_system_access/file_system_access_handle_base.cc
@@ -396,8 +396,7 @@
   DCHECK_EQ(GetEffectiveWritePermissionStatus(),
             blink::mojom::PermissionStatus::GRANTED);
 
-  if (!manager()->IsSafePathComponent(
-          url().type(), context_.storage_key.origin(), new_entry_name)) {
+  if (!manager()->IsSafePathComponent(url().type(), new_entry_name)) {
     std::move(callback).Run(file_system_access_error::FromStatus(
         blink::mojom::FileSystemAccessStatus::kInvalidArgument));
     return;
@@ -459,8 +458,7 @@
   }
 
   if (!manager()->IsSafePathComponent(
-          resolved_destination_directory->url().type(),
-          context_.storage_key.origin(), new_entry_name)) {
+          resolved_destination_directory->url().type(), new_entry_name)) {
     std::move(callback).Run(file_system_access_error::FromStatus(
         blink::mojom::FileSystemAccessStatus::kInvalidArgument));
     return;
diff --git a/content/browser/file_system_access/file_system_access_manager_impl.cc b/content/browser/file_system_access/file_system_access_manager_impl.cc
index d9688341..289878c 100644
--- a/content/browser/file_system_access/file_system_access_manager_impl.cc
+++ b/content/browser/file_system_access/file_system_access_manager_impl.cc
@@ -2261,7 +2261,6 @@
 
 bool FileSystemAccessManagerImpl::IsSafePathComponent(
     storage::FileSystemType type,
-    const url::Origin& origin,
     const std::string& name) {
   // This method is similar to net::IsSafePortablePathComponent, with a few
   // notable differences where the net version does not consider names safe
@@ -2355,7 +2354,7 @@
   // components/safe_browsing/content/resources/download_file_types.asciipb,
   // are considered unsafe, with an exception of ".local" extensions.
   if (extension_lower != FILE_PATH_LITERAL("local") && permission_context_ &&
-      permission_context_->IsFileTypeDangerous(component, origin)) {
+      permission_context_->IsFileTypeDangerous(component)) {
     return false;
   }
 
diff --git a/content/browser/file_system_access/file_system_access_manager_impl.h b/content/browser/file_system_access/file_system_access_manager_impl.h
index 9a244dd8..5d5aa093 100644
--- a/content/browser/file_system_access/file_system_access_manager_impl.h
+++ b/content/browser/file_system_access/file_system_access_manager_impl.h
@@ -429,7 +429,6 @@
   // TODO(crbug.com/40159607): Merge this with
   // net::IsSafePortablePathComponent.
   bool IsSafePathComponent(storage::FileSystemType type,
-                           const url::Origin& origin,
                            const std::string& name);
 
   // Invokes `method` on the correct sequence on the FileSystemOperationRunner,
diff --git a/content/browser/file_system_access/mock_file_system_access_permission_context.cc b/content/browser/file_system_access/mock_file_system_access_permission_context.cc
index e592219..a7f60300 100644
--- a/content/browser/file_system_access/mock_file_system_access_permission_context.cc
+++ b/content/browser/file_system_access/mock_file_system_access_permission_context.cc
@@ -30,9 +30,8 @@
 }
 
 bool MockFileSystemAccessPermissionContext::IsFileTypeDangerous(
-    const base::FilePath& path,
-    const url::Origin& origin) {
-  return IsFileTypeDangerous_(path, origin);
+    const base::FilePath& path) {
+  return IsFileTypeDangerous_(path);
 }
 
 }  // namespace content
diff --git a/content/browser/file_system_access/mock_file_system_access_permission_context.h b/content/browser/file_system_access/mock_file_system_access_permission_context.h
index 96f6e56..3d53529 100644
--- a/content/browser/file_system_access/mock_file_system_access_permission_context.h
+++ b/content/browser/file_system_access/mock_file_system_access_permission_context.h
@@ -62,11 +62,8 @@
                GlobalRenderFrameHostId frame_id,
                base::OnceCallback<void(AfterWriteCheckResult)>& callback));
 
-  bool IsFileTypeDangerous(const base::FilePath& path,
-                           const url::Origin& origin) override;
-  MOCK_METHOD(bool,
-              IsFileTypeDangerous_,
-              (const base::FilePath& path, const url::Origin& origin));
+  bool IsFileTypeDangerous(const base::FilePath& path) override;
+  MOCK_METHOD(bool, IsFileTypeDangerous_, (const base::FilePath& path));
 
   MOCK_METHOD((base::expected<void, std::string>),
               CanShowFilePicker,
diff --git a/content/public/browser/file_system_access_permission_context.h b/content/public/browser/file_system_access_permission_context.h
index 292dc56..a876a9dc 100644
--- a/content/public/browser/file_system_access_permission_context.h
+++ b/content/public/browser/file_system_access_permission_context.h
@@ -186,8 +186,7 @@
 
   // Returns whether the file type is considered dangerous. This is used to
   // block file operations from creating or accessing these file types.
-  virtual bool IsFileTypeDangerous(const base::FilePath& path,
-                                   const url::Origin& origin) = 0;
+  virtual bool IsFileTypeDangerous(const base::FilePath& path) = 0;
 
   // Returns whether the given RFH can use file picker.
   virtual base::expected<void, std::string> CanShowFilePicker(
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
... (file diff truncated)
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 rj...@google.com

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