CVE-2026-79276
Overview
Background
- File System Access grants
- Read/write permissions a site obtains (e.g. via
showSaveFilePicker()) to access files on the user’s disk, managed byChromeFileSystemAccessPermissionContext. - 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
HostContentSettingsMapback 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_SETTINGSpath.
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.
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
- 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 confirmsOriginHasReadAccess/OriginHasWriteAccess). - User resets permissions
Believing they are revoking site access, the user runs a site-permissions reset via chrome://settings/reset, invoking
ProfileResetter::ResetContentSettings. - Grant survives the reset
The reset cleared
HostContentSettingsMapbut not the permission context’s in-memory grants, so the still-open tab retained its read/write file access. - 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
(inferred).Changed Functions
| Function | Change | Notes |
|---|---|---|
PinnedTabsResetTestchrome/browser/profile_resetter/profile_resetter_browsertest.cc |
modified | |
ifchrome/browser/profile_resetter/BUILD.gn |
modified |
Files Changed
chrome/browser/profile_resetter/profile_resetter.ccchrome/browser/profile_resetter/profile_resetter_browsertest.ccchrome/browser/profile_resetter/BUILD.gn
Audit Directions
- State outside the settings storeAudit 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 pathsCompare 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 lifetimeLook 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.
Patch
From c802d09da19967a00e4aee491b76e4707f9bb8e7 Mon Sep 17 00:00:00 2001 From: Eriko Kurimoto <[email protected]> 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é <[email protected]> Commit-Queue: Eriko Kurimoto <[email protected]> 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) {
Regression Test / PoC
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,
Original Bug Report
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.
- A user visits an attacker-controlled page (e.g.,
https://attacker.example). - The page calls
window.showSaveFilePicker()and the user chooses a local file, granting active write permission. - The attacker page stores the file handle in JavaScript (keeping the Mojo connection and the corresponding browser-side
FileSystemAccessPermissionGrantImplref-counts alive). - In another tab, the user goes to
chrome://settings/resetand clicks “Reset settings”. - 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()). - Since the browser-side active grant status was not revoked,
GetStatus()continues to returnPermissionStatus::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.