Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactImproper privilege management in FileSystem
DescriptionImproper privilege management in FileSystem
ComponentFileSystem
Bug ClassLogic Error
Tracker517580738
Fix commitc802d09da199 (chromium/src) +60/-0
CISA KEVNot listed
CreditedGoogle
Disclosed2026-08-25

Changed Functions

FunctionChangeNotes
if
chrome/browser/profile_resetter/BUILD.gn
modified
PinnedTabsResetTest
chrome/browser/profile_resetter/profile_resetter_browsertest.cc
modified

Files Changed

  • chrome/browser/profile_resetter/BUILD.gn
  • chrome/browser/profile_resetter/profile_resetter.cc
  • chrome/browser/profile_resetter/profile_resetter_browsertest.cc
From c802d09da19967a00e4aee491b76e4707f9bb8e7 Mon Sep 17 00:00:00 2001
From: Eriko Kurimoto <elkurin@google.com>
Date: Wed, 01 Jul 2026 20:42:23 -0700
Subject: [PATCH] Revoke active File System Access grants on profile reset

ProfileResetter::ResetContentSettings clears every type in
HostContentSettingsMap, but ChromeFileSystemAccessPermissionContext
keeps active read/write grants in an in-memory map outside the
content-settings store. Clear Browsing Data already calls
RevokeAllActiveGrants() for the DATA_TYPE_CONTENT_SETTINGS path; mirror
that here so that resetting site permissions via chrome://settings/reset
also drops any in-memory File System Access grants for still-open tabs.

Add a browser test that obtains a write grant via showSaveFilePicker(),
runs the CONTENT_SETTINGS reset, and checks that the origin no longer
has read or write access.

Bug: 517580738
Change-Id: I9e26e10388d6d2263eb3541603ea005193badbac
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8029722
Reviewed-by: Dominic Battré <battre@chromium.org>
Commit-Queue: Eriko Kurimoto <elkurin@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1655878}
---

diff --git a/chrome/browser/profile_resetter/BUILD.gn b/chrome/browser/profile_resetter/BUILD.gn
index 14af49d..5777f77 100644
--- a/chrome/browser/profile_resetter/BUILD.gn
+++ b/chrome/browser/profile_resetter/BUILD.gn
@@ -60,6 +60,7 @@
       "//chrome/browser/browsing_data:constants",
       "//chrome/browser/content_settings:content_settings_factory",
       "//chrome/browser/extensions",
+      "//chrome/browser/file_system_access",
       "//chrome/browser/google",
       "//chrome/browser/prefs:util",
       "//chrome/browser/profiles:profile",
@@ -166,12 +167,15 @@
       ":profile_resetter",
       ":profile_resetter_test_base",
       "//base",
+      "//chrome/browser/file_system_access",
       "//chrome/browser/ui",
       "//chrome/browser/ui/tabs:tab_strip",
       "//chrome/test:test_support_ui",
       "//content/test:test_support",
       "//testing/gmock",
       "//testing/gtest",
+      "//ui/shell_dialogs",
+      "//url",
     ]
 
     if (is_chromeos) {
diff --git a/chrome/browser/profile_resetter/profile_resetter.cc b/chrome/browser/profile_resetter/profile_resetter.cc
index f10ed08b..37b80b3 100644
--- a/chrome/browser/profile_resetter/profile_resetter.cc
+++ b/chrome/browser/profile_resetter/profile_resetter.cc
@@ -21,6 +21,8 @@
 #include "chrome/browser/browsing_data/chrome_browsing_data_remover_constants.h"
 #include "chrome/browser/content_settings/host_content_settings_map_factory.h"
 #include "chrome/browser/extensions/extension_service.h"
+#include "chrome/browser/file_system_access/chrome_file_system_access_permission_context.h"
+#include "chrome/browser/file_system_access/file_system_access_permission_context_factory.h"
 #include "chrome/browser/google/google_brand.h"
 #include "chrome/browser/net/system_network_context_manager.h"
 #include "chrome/browser/profile_resetter/brandcode_config_fetcher.h"
@@ -313,6 +315,15 @@
     map->SetDefaultContentSetting(info->website_settings_info()->type(),
                                   CONTENT_SETTING_DEFAULT);
   }
+
+  // Active File System Access grants are kept in memory by the permission
+  // context rather than in HostContentSettingsMap, so they need to be revoked
+  // explicitly.
+  if (auto* permission_context =
+          FileSystemAccessPermissionContextFactory::GetForProfile(profile_)) {
+    permission_context->RevokeAllActiveGrants();
+  }
+
   MarkAsDone(CONTENT_SETTINGS);
 }
 
diff --git a/chrome/browser/profile_resetter/profile_resetter_browsertest.cc b/chrome/browser/profile_resetter/profile_resetter_browsertest.cc
index d776c3b..48a77c4 100644
--- a/chrome/browser/profile_resetter/profile_resetter_browsertest.cc
+++ b/chrome/browser/profile_resetter/profile_resetter_browsertest.cc
@@ -6,8 +6,13 @@
 
 #include <memory>
 
+#include "base/files/file_util.h"
+#include "base/files/scoped_temp_dir.h"
 #include "base/functional/bind.h"
 #include "base/memory/raw_ptr.h"
+#include "base/threading/thread_restrictions.h"
+#include "chrome/browser/file_system_access/chrome_file_system_access_permission_context.h"
+#include "chrome/browser/file_system_access/file_system_access_permission_context_factory.h"
 #include "chrome/browser/profile_resetter/profile_resetter_test_base.h"
 #include "chrome/browser/profiles/profile.h"
 #include "chrome/browser/ui/browser.h"
@@ -16,6 +21,8 @@
 #include "content/public/browser/browser_context.h"
 #include "content/public/browser/storage_partition.h"
 #include "content/public/test/browser_test.h"
+#include "content/public/test/browser_test_utils.h"
+#include "content/public/test/file_system_chooser_test_helpers.h"
 #include "content/public/test/test_utils.h"
 #include "mojo/public/cpp/bindings/remote.h"
 #include "net/cookies/canonical_cookie.h"
@@ -23,6 +30,8 @@
 #include "net/cookies/cookie_util.h"
 #include "services/network/public/mojom/cookie_manager.mojom.h"
 #include "services/network/public/mojom/network_context.mojom.h"
+#include "ui/shell_dialogs/select_file_dialog.h"
+#include "url/origin.h"
 
 #if BUILDFLAG(IS_CHROMEOS)
 #include "base/containers/to_vector.h"
@@ -178,6 +187,42 @@
   EXPECT_FALSE(tester.GetCookie(kCookieHostname, &cookie));
 }
 
+IN_PROC_BROWSER_TEST_F(
+    ProfileResetTest,
+    ResetContentSettingsRevokesActiveFileSystemAccessGrants) {
+  base::ScopedAllowBlockingForTesting allow_blocking;
+  base::ScopedTempDir temp_dir;
+  ASSERT_TRUE(temp_dir.CreateUniqueTempDir());
+  base::FilePath test_file;
+  ASSERT_TRUE(base::CreateTemporaryFileInDir(temp_dir.GetPath(), &test_file));
+
+  ASSERT_TRUE(embedded_test_server()->Start());
+  const GURL url = embedded_test_server()->GetURL("/title1.html");
+  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url));
+
+  ui::SelectFileDialog::SetFactory(
+      std::make_unique<content::FakeSelectFileDialogFactory>(
+          std::vector<base::FilePath>{test_file}));
+  content::WebContents* web_contents =
+      browser()->tab_strip_model()->GetActiveWebContents();
+  ASSERT_TRUE(content::ExecJs(
+      web_contents,
+      "(async () => { self.handle = await self.showSaveFilePicker(); })()"));
+  ui::SelectFileDialog::SetFactory(nullptr);
+
+  ChromeFileSystemAccessPermissionContext* permission_context =
+      FileSystemAccessPermissionContextFactory::GetForProfile(
+          browser()->profile());
+  const url::Origin origin = url::Origin::Create(url);
+  ASSERT_TRUE(permission_context->OriginHasReadAccess(origin));
+  ASSERT_TRUE(permission_context->OriginHasWriteAccess(origin));
+
+  ResetAndWait(ProfileResetter::CONTENT_SETTINGS);
+
+  EXPECT_FALSE(permission_context->OriginHasReadAccess(origin));
+  EXPECT_FALSE(permission_context->OriginHasWriteAccess(origin));
+}
+
 // PinnedTabsResetTest --------------------------------------------------------
 
 class PinnedTabsResetTest : public InProcessBrowserTest,
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/chrome/browser/profile_resetter/profile_resetter_browsertest.cc b/chrome/browser/profile_resetter/profile_resetter_browsertest.cc
index d776c3b..48a77c4 100644
--- a/chrome/browser/profile_resetter/profile_resetter_browsertest.cc
+++ b/chrome/browser/profile_resetter/profile_resetter_browsertest.cc
@@ -6,8 +6,13 @@
 
 #include <memory>
 
+#include "base/files/file_util.h"
+#include "base/files/scoped_temp_dir.h"
 #include "base/functional/bind.h"
 #include "base/memory/raw_ptr.h"
+#include "base/threading/thread_restrictions.h"
+#include "chrome/browser/file_system_access/chrome_file_system_access_permission_context.h"
+#include "chrome/browser/file_system_access/file_system_access_permission_context_factory.h"
 #include "chrome/browser/profile_resetter/profile_resetter_test_base.h"
 #include "chrome/browser/profiles/profile.h"
 #include "chrome/browser/ui/browser.h"
@@ -16,6 +21,8 @@
 #include "content/public/browser/browser_context.h"
 #include "content/public/browser/storage_partition.h"
 #include "content/public/test/browser_test.h"
+#include "content/public/test/browser_test_utils.h"
+#include "content/public/test/file_system_chooser_test_helpers.h"
 #include "content/public/test/test_utils.h"
 #include "mojo/public/cpp/bindings/remote.h"
 #include "net/cookies/canonical_cookie.h"
@@ -23,6 +30,8 @@
 #include "net/cookies/cookie_util.h"
 #include "services/network/public/mojom/cookie_manager.mojom.h"
 #include "services/network/public/mojom/network_context.mojom.h"
+#include "ui/shell_dialogs/select_file_dialog.h"
+#include "url/origin.h"
 
 #if BUILDFLAG(IS_CHROMEOS)
 #include "base/containers/to_vector.h"
@@ -178,6 +187,42 @@
   EXPECT_FALSE(tester.GetCookie(kCookieHostname, &cookie));
 }
 
+IN_PROC_BROWSER_TEST_F(
+    ProfileResetTest,
+    ResetContentSettingsRevokesActiveFileSystemAccessGrants) {
+  base::ScopedAllowBlockingForTesting allow_blocking;
+  base::ScopedTempDir temp_dir;
+  ASSERT_TRUE(temp_dir.CreateUniqueTempDir());
+  base::FilePath test_file;
+  ASSERT_TRUE(base::CreateTemporaryFileInDir(temp_dir.GetPath(), &test_file));
+
+  ASSERT_TRUE(embedded_test_server()->Start());
+  const GURL url = embedded_test_server()->GetURL("/title1.html");
+  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url));
+
+  ui::SelectFileDialog::SetFactory(
+      std::make_unique<content::FakeSelectFileDialogFactory>(
+          std::vector<base::FilePath>{test_file}));
+  content::WebContents* web_contents =
+      browser()->tab_strip_model()->GetActiveWebContents();
+  ASSERT_TRUE(content::ExecJs(
+      web_contents,
+      "(async () => { self.handle = await self.showSaveFilePicker(); })()"));
+  ui::SelectFileDialog::SetFactory(nullptr);
+
+  ChromeFileSystemAccessPermissionContext* permission_context =
+      FileSystemAccessPermissionContextFactory::GetForProfile(
+          browser()->profile());
+  const url::Origin origin = url::Origin::Create(url);
+  ASSERT_TRUE(permission_context->OriginHasReadAccess(origin));
+  ASSERT_TRUE(permission_context->OriginHasWriteAccess(origin));
+
+  ResetAndWait(ProfileResetter::CONTENT_SETTINGS);
+
+  EXPECT_FALSE(permission_context->OriginHasReadAccess(origin));
+  EXPECT_FALSE(permission_context->OriginHasWriteAccess(origin));
+}
+
 // PinnedTabsResetTest --------------------------------------------------------
 
 class PinnedTabsResetTest : public InProcessBrowserTest,
Loading diff…

Original Bug Report

reported by vm...@google.com

Potential bypass of Profile Reset via active File System Access grants

Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: When performing a profile reset, in-memory File System Access (FSA) active permission grants are not revoked. While the persistent settings are deleted from HostContentSettingsMap, the active grants remain in-memory outside it, potentially allowing open tabs to retain unauthorized file read/write access.

Affected files:

  • chrome/browser/profile_resetter/profile_resetter.cc

Estimated timestamp from git blame: 2024-03-07

Root Cause Analysis

ChromeFileSystemAccessPermissionContext maintains active File System Access (FSA) read/write permission grants in an in-memory map (active_permissions_map_). This map resides entirely outside of HostContentSettingsMap (HCSM).

When a user triggers a profile reset via chrome://settings/reset, ProfileResetter::ResetContentSettings() is called to revert content settings to defaults:

// chrome/browser/profile_resetter/profile_resetter.cc
void ProfileResetter::ResetContentSettings() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  HostContentSettingsMap* map =
      HostContentSettingsMapFactory::GetForProfile(profile_);

  for (const content_settings::WebsiteSettingsInfo* info :
       *content_settings::WebsiteSettingsRegistry::GetInstance()) {
    map->ClearSettingsForOneType(info->type());
  }
  ...
}

While clearing HCSM removes the persistent permission registries (FILE_SYSTEM_ACCESS_CHOOSER_DATA and FILE_SYSTEM_ACCESS_EXTENDED_PERMISSION), ProfileResetter fails to clear the active in-memory grants. Because ChromeFileSystemAccessPermissionContext does not observe or receive automatic notifications of HCSM changes, the active grants inside active_permissions_map_ continue to remain active with their status set to PermissionStatus::GRANTED.

In contrast, the Clear Browsing Data flow explicitly handles this out-of-HCSM cleanup inside chrome/browser/browsing_data/chrome_browsing_data_remover_delegate.cc:

if (remove_mask & constants::DATA_TYPE_CONTENT_SETTINGS) {
  ...
  if (auto* permission_context =
          FileSystemAccessPermissionContextFactory::GetForProfile(profile_)) {
    permission_context->RevokeAllActiveGrants();
  }
}

Potential Attack Scenario / Reproduction Steps

Note: The following are suggested/potential steps compiled from static code analysis, as our tooling environment does not support executing runtime tests or running code.

  1. A user visits an attacker-controlled page (e.g., https://attacker.example).
  2. The page calls window.showSaveFilePicker() and the user chooses a local file, granting active write permission.
  3. The attacker page stores the file handle in JavaScript (keeping the Mojo connection and the corresponding browser-side FileSystemAccessPermissionGrantImpl ref-counts alive).
  4. In another tab, the user goes to chrome://settings/reset and clicks “Reset settings”.
  5. After the reset completes, the attacker page (still open in the background/foreground) attempts to write to the file via the retained handle (e.g., calling handle.createWritable()).
  6. Since the browser-side active grant status was not revoked, GetStatus() continues to return PermissionStatus::GRANTED, bypassing the reset guard setting and silently executing the write operation.

Security Impact

This is a potential permission/privilege persistence issue. A user who performs a profile reset expects all active capabilities and permissions across all sites to be revoked. Surviving active FSA grants violate this expectation, potentially letting untrusted pages silently read or write to previously-granted local files or directories for the remainder of the tab’s lifetime.

Suggested Remediation

Modify ProfileResetter::ResetContentSettings to explicitly retrieve the ChromeFileSystemAccessPermissionContext and call RevokeAllActiveGrants() when resetting content settings, matching the behavior of the Clear Browsing Data flow:

// In chrome/browser/profile_resetter/profile_resetter.cc
#include "chrome/browser/file_system_access/chrome_file_system_access_permission_context.h"
#include "chrome/browser/file_system_access/file_system_access_permission_context_factory.h"

void ProfileResetter::ResetContentSettings() {
  ...
  if (auto* permission_context =
          FileSystemAccessPermissionContextFactory::GetForProfile(profile_)) {
    permission_context->RevokeAllActiveGrants();
  }
  MarkAsDone(CONTENT_SETTINGS);
}

Evaluated with Chrome root at commit: 5133b93d189b383c37805b1cf3a9d2dbfe8d7379


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