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
ImpactImproper privilege management in FileSystem
DescriptionImproper privilege management in FileSystem
ComponentFileSystem
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.
Tracker517580738
Fix commitc802d09da199 (chromium/src) +60/-0
CWECWE-269
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-28 (per vendor advisory)
Fix landed2026-07-02 (public git)
Disclosed2026-08-25
Patch gap54 days — fix public in source before the advisory

Background

File System Access grants
Read/write permissions a site obtains (e.g. via showSaveFilePicker()) to access files on the user’s disk, managed by ChromeFileSystemAccessPermissionContext.
In-memory active grants
Unlike persisted site settings, active File System Access grants for still-open tabs live in an in-memory map inside the permission context, outside HostContentSettingsMap.
`ProfileResetter::ResetContentSettings`
The routine behind chrome://settings/reset that clears every content-settings type in HostContentSettingsMap back to default.
`RevokeAllActiveGrants`
The permission-context method that drops the in-memory read/write grants; Clear Browsing Data already invoked it for the DATA_TYPE_CONTENT_SETTINGS path.

Root Cause Analysis

ProfileResetter::ResetContentSettings (named in the commit; its body is the edited hunk) iterated over content-settings types and reset each via map->SetDefaultContentSetting(..., CONTENT_SETTING_DEFAULT), then called MarkAsDone(CONTENT_SETTINGS). Because active File System Access grants are held in the permission context’s in-memory map rather than in HostContentSettingsMap, clearing the content-settings store left those grants intact. The violated invariant is that resetting a profile’s site permissions must actually revoke all site access, including live File System Access grants for still-open tabs.

The fix adds, before MarkAsDone, a lookup of the permission context via FileSystemAccessPermissionContextFactory::GetForProfile(profile_) and a call to permission_context->RevokeAllActiveGrants(), mirroring the existing Clear Browsing Data behavior so a reset now drops in-memory grants as well.

Key insight
ResetContentSettings cleared only HostContentSettingsMap and forgot the permission context’s separate in-memory File System Access grants; the fix calls RevokeAllActiveGrants() during the reset so those grants are revoked too.

Attack Path

  1. Obtain a grant A site the user is visiting acquires a File System Access read/write grant, e.g. via showSaveFilePicker(), giving it live access to a file (the browser test confirms OriginHasReadAccess/OriginHasWriteAccess).
  2. User resets permissions Believing they are revoking site access, the user runs a site-permissions reset via chrome://settings/reset, invoking ProfileResetter::ResetContentSettings.
  3. Grant survives the reset The reset cleared HostContentSettingsMap but not the permission context’s in-memory grants, so the still-open tab retained its read/write file access.
  4. Continued access The origin keeps exercising the stale grant against the user’s file despite the reset, the improper-privilege-management defect the advisory describes.

Impact Assessment

An origin with a still-open tab retains active File System Access read/write grants after the user performs a content-settings reset, so site file access persists when the user expects it revoked, an improper-privilege-management issue in FileSystem as the advisory states. Preconditions are a previously granted File System Access permission and a live tab holding it at reset time. The advisory does not report in-the-wild exploitation, and any use of the retained access to reach sensitive files beyond what was originally granted is (inferred).

Changed Functions

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

Files Changed

  • chrome/browser/profile_resetter/profile_resetter.cc
  • chrome/browser/profile_resetter/profile_resetter_browsertest.cc
  • chrome/browser/profile_resetter/BUILD.gn

Audit Directions

  • State outside the settings store
    Audit reset/clear flows for permissions or grants held in memory or elsewhere outside HostContentSettingsMap; clearing the map alone may leave live access intact.
  • Parity between clearing paths
    Compare Clear Browsing Data and Profile Reset (and any other revocation entry points) to ensure each revokes the same set of grants for a given data type.
  • Still-open-tab lifetime
    Look for permissions whose in-memory representation outlives a settings change while a tab is open, so a UI action that implies revocation does not actually drop the capability.
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/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,
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) {
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