CVE-2026-11258
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
forchrome/browser/file_system_access/chrome_file_system_access_permission_context.cc |
modified |
Files Changed
chrome/browser/file_system_access/chrome_file_system_access_permission_context.ccchrome/browser/file_system_access/chrome_file_system_access_permission_context_unittest.cc
Patch
From a4c6a71e544ce5ba74720b57432fe1a84721a83a Mon Sep 17 00:00:00 2001
From: Andrew Paseltiner <apaseltiner@chromium.org>
Date: Wed, 08 Apr 2026 06:09:20 -0700
Subject: [PATCH] FSA: Clear downgraded_read_paths on active grant revocation
ChromeFileSystemAccessPermissionContext fails to clear the
`downgraded_read_paths` set when a user explicitly revokes File System
Access permissions. This stale state can be exploited to silently
escalate a subsequent write-only permission prompt into full read-write
access, bypassing the user's revocation intent. This appears to have
been an oversight in crrev.com/c/6677249.
This CL fixes the state leak by updating `RevokeActiveGrants` and
`RevokeAllActiveGrants` to clear the `downgraded_read_paths` set
appropriately.
Specifically:
- `RevokeActiveGrants(origin, file_path)`: Clears the set for the origin
if `file_path` is empty, or erases the specific `file_path` if
provided.
- `RevokeAllActiveGrants()`: Clears the set for all origins in the
active permissions map.
Regression tests are added to verify these cases.
Fixed: 499078161
Change-Id: I31e6c9123e9f007c92e294eff71db1830d7fd029
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7730146
Reviewed-by: Judith Hemp <hempjudith@google.com>
Commit-Queue: Andrew Paseltiner <apaseltiner@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1611427}
---
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 fc75611e..b9e0a14 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
@@ -1571,6 +1571,18 @@
auto origin_it = active_permissions_map_.find(origin);
if (origin_it != active_permissions_map_.end()) {
OriginState& origin_state = origin_it->second;
+
+ if (file_path.empty()) {
+ if (!origin_state.downgraded_read_paths.empty()) {
+ origin_state.downgraded_read_paths.clear();
+ grant_revoked = true;
+ }
+ } else {
+ if (origin_state.downgraded_read_paths.erase(file_path)) {
+ grant_revoked = true;
+ }
+ }
+
for (auto grant_iter = origin_state.read_grants.begin(),
grant_end = origin_state.read_grants.end();
grant_iter != grant_end;) {
@@ -1615,6 +1627,8 @@
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
for (auto& [origin, origin_state] : active_permissions_map_) {
+ origin_state.downgraded_read_paths.clear();
+
// Only update `persisted_grant_status` if the state has not already been
// set via tab backgrounding. We do this before iterating over grants so
// `FileSystemAccessPermissionGrant::Observer`s can update their state
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 16969a4d..f729660 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
@@ -3694,6 +3694,136 @@
EXPECT_EQ(old_file2_read_grant->GetStatus(), PermissionStatus::ASK);
EXPECT_EQ(old_file2_write_grant->GetStatus(), PermissionStatus::ASK);
}
+
+// Regression test for crbug.com/499078161.
+TEST_F(ChromeFileSystemAccessPermissionContextTest,
+ RevokeActiveGrants_ClearsDowngradedReadPaths) {
+ base::test::ScopedFeatureList feature_list;
+ feature_list.InitAndEnableFeature(
+ blink::features::kFileSystemAccessRevokeReadOnRemove);
+ FileSystemAccessPermissionRequestManager::FromWebContents(web_contents())
+ ->set_auto_response_for_test(PermissionAction::GRANTED);
+ permission_context()->SetOriginHasExtendedPermissionForTesting(kTestOrigin);
+
+ // Sets up a file path to be the test target.
+ const auto file_path_info =
+ PathInfo(kTestPathInfo.path.AppendASCII("test_file.txt"));
+
+ // Grant read and write permission to the file path.
+ EXPECT_EQ(permission_context()
+ ->GetReadPermissionGrant(kTestOrigin, file_path_info,
+ HandleType::kFile, UserAction::kSave)
+ ->GetStatus(),
+ PermissionStatus::GRANTED);
+ EXPECT_EQ(permission_context()
+ ->GetWritePermissionGrant(kTestOrigin, file_path_info,
+ HandleType::kFile, UserAction::kSave)
+ ->GetStatus(),
+ PermissionStatus::GRANTED);
+
+ // 1. Revoke the read permission for the file path by calling NotifyEntryRemoved.
+ // This adds the path to downgraded_read_paths.
+ permission_context()->NotifyEntryRemoved(kTestOrigin, file_path_info);
+
+ // Verify the path is added to downgraded_read_paths.
+ EXPECT_TRUE(permission_context()->IsPathInDowngradedReadPathsForTesting(
+ kTestOrigin, file_path_info.path));
+
+ // 2. Revoke all active grants for the origin. This should clear downgraded_read_paths.
+ permission_context()->RevokeActiveGrantsForTesting(kTestOrigin);
+
+ // Verify the path is removed from downgraded_read_paths.
+ EXPECT_FALSE(permission_context()->IsPathInDowngradedReadPathsForTesting(
+ kTestOrigin, file_path_info.path));
+}
+
+// Regression test for crbug.com/499078161.
+TEST_F(ChromeFileSystemAccessPermissionContextTest,
+ RevokeActiveGrants_SpecificPath_ClearsDowngradedReadPaths) {
+ base::test::ScopedFeatureList feature_list;
+ feature_list.InitAndEnableFeature(
+ blink::features::kFileSystemAccessRevokeReadOnRemove);
+ FileSystemAccessPermissionRequestManager::FromWebContents(web_contents())
+ ->set_auto_response_for_test(PermissionAction::GRANTED);
+ permission_context()->SetOriginHasExtendedPermissionForTesting(kTestOrigin);
+
+ // Sets up file paths to be the test targets.
+ const auto file_path_info1 =
+ PathInfo(kTestPathInfo.path.AppendASCII("test_file1.txt"));
+ const auto file_path_info2 =
+ PathInfo(kTestPathInfo.path.AppendASCII("test_file2.txt"));
+
+ // Grant read and write permission to the file paths.
+ EXPECT_EQ(permission_context()
+ ->GetReadPermissionGrant(kTestOrigin, file_path_info1,
+ HandleType::kFile, UserAction::kSave)
+ ->GetStatus(),
+ PermissionStatus::GRANTED);
+ EXPECT_EQ(permission_context()
+ ->GetReadPermissionGrant(kTestOrigin, file_path_info2,
+ HandleType::kFile, UserAction::kSave)
+ ->GetStatus(),
+ PermissionStatus::GRANTED);
+
+ // 1. Revoke the read permission for the file paths by calling NotifyEntryRemoved.
+ // This adds the paths to downgraded_read_paths.
+ permission_context()->NotifyEntryRemoved(kTestOrigin, file_path_info1);
+ permission_context()->NotifyEntryRemoved(kTestOrigin, file_path_info2);
+
+ // Verify the paths are added to downgraded_read_paths.
+ EXPECT_TRUE(permission_context()->IsPathInDowngradedReadPathsForTesting(
+ kTestOrigin, file_path_info1.path));
+ EXPECT_TRUE(permission_context()->IsPathInDowngradedReadPathsForTesting(
+ kTestOrigin, file_path_info2.path));
+
+ // 2. Revoke active grant for a specific path.
+ permission_context()->RevokeActiveGrantsForTesting(kTestOrigin,
+ file_path_info1.path);
+
+ // Verify file_path_info1 is removed from downgraded_read_paths.
+ EXPECT_FALSE(permission_context()->IsPathInDowngradedReadPathsForTesting(
+ kTestOrigin, file_path_info1.path));
+ // Verify file_path_info2 is STILL in downgraded_read_paths.
+ EXPECT_TRUE(permission_context()->IsPathInDowngradedReadPathsForTesting(
+ kTestOrigin, file_path_info2.path));
+}
+
+// Regression test for crbug.com/499078161.
+TEST_F(ChromeFileSystemAccessPermissionContextTest,
+ RevokeAllActiveGrants_ClearsDowngradedReadPaths) {
+ base::test::ScopedFeatureList feature_list;
+ feature_list.InitAndEnableFeature(
+ blink::features::kFileSystemAccessRevokeReadOnRemove);
+ FileSystemAccessPermissionRequestManager::FromWebContents(web_contents())
+ ->set_auto_response_for_test(PermissionAction::GRANTED);
+ permission_context()->SetOriginHasExtendedPermissionForTesting(kTestOrigin);
+
+ // Sets up a file path to be the test target.
+ const auto file_path_info =
+ PathInfo(kTestPathInfo.path.AppendASCII("test_file.txt"));
+
+ // Grant read and write permission to the file path.
+ EXPECT_EQ(permission_context()
+ ->GetReadPermissionGrant(kTestOrigin, file_path_info,
+ HandleType::kFile, UserAction::kSave)
+ ->GetStatus(),
+ PermissionStatus::GRANTED);
+
+ // 1. Revoke the read permission for the file path by calling NotifyEntryRemoved.
+ // This adds the path to downgraded_read_paths.
+ permission_context()->NotifyEntryRemoved(kTestOrigin, file_path_info);
+
+ // Verify the path is added to downgraded_read_paths.
+ EXPECT_TRUE(permission_context()->IsPathInDowngradedReadPathsForTesting(
+ kTestOrigin, file_path_info.path));
+
+ // 2. Revoke all active grants. This should clear downgraded_read_paths for all origins.
+ permission_context()->RevokeAllActiveGrants();
+
+ // Verify the path is removed from downgraded_read_paths.
Regression Test / PoC
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 16969a4d..f729660 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
@@ -3694,6 +3694,136 @@
EXPECT_EQ(old_file2_read_grant->GetStatus(), PermissionStatus::ASK);
EXPECT_EQ(old_file2_write_grant->GetStatus(), PermissionStatus::ASK);
}
+
+// Regression test for crbug.com/499078161.
+TEST_F(ChromeFileSystemAccessPermissionContextTest,
+ RevokeActiveGrants_ClearsDowngradedReadPaths) {
+ base::test::ScopedFeatureList feature_list;
+ feature_list.InitAndEnableFeature(
+ blink::features::kFileSystemAccessRevokeReadOnRemove);
+ FileSystemAccessPermissionRequestManager::FromWebContents(web_contents())
+ ->set_auto_response_for_test(PermissionAction::GRANTED);
+ permission_context()->SetOriginHasExtendedPermissionForTesting(kTestOrigin);
+
+ // Sets up a file path to be the test target.
+ const auto file_path_info =
+ PathInfo(kTestPathInfo.path.AppendASCII("test_file.txt"));
+
+ // Grant read and write permission to the file path.
+ EXPECT_EQ(permission_context()
+ ->GetReadPermissionGrant(kTestOrigin, file_path_info,
+ HandleType::kFile, UserAction::kSave)
+ ->GetStatus(),
+ PermissionStatus::GRANTED);
+ EXPECT_EQ(permission_context()
+ ->GetWritePermissionGrant(kTestOrigin, file_path_info,
+ HandleType::kFile, UserAction::kSave)
+ ->GetStatus(),
+ PermissionStatus::GRANTED);
+
+ // 1. Revoke the read permission for the file path by calling NotifyEntryRemoved.
+ // This adds the path to downgraded_read_paths.
+ permission_context()->NotifyEntryRemoved(kTestOrigin, file_path_info);
+
+ // Verify the path is added to downgraded_read_paths.
+ EXPECT_TRUE(permission_context()->IsPathInDowngradedReadPathsForTesting(
+ kTestOrigin, file_path_info.path));
+
+ // 2. Revoke all active grants for the origin. This should clear downgraded_read_paths.
+ permission_context()->RevokeActiveGrantsForTesting(kTestOrigin);
+
+ // Verify the path is removed from downgraded_read_paths.
+ EXPECT_FALSE(permission_context()->IsPathInDowngradedReadPathsForTesting(
+ kTestOrigin, file_path_info.path));
+}
+
+// Regression test for crbug.com/499078161.
+TEST_F(ChromeFileSystemAccessPermissionContextTest,
+ RevokeActiveGrants_SpecificPath_ClearsDowngradedReadPaths) {
+ base::test::ScopedFeatureList feature_list;
+ feature_list.InitAndEnableFeature(
+ blink::features::kFileSystemAccessRevokeReadOnRemove);
+ FileSystemAccessPermissionRequestManager::FromWebContents(web_contents())
+ ->set_auto_response_for_test(PermissionAction::GRANTED);
+ permission_context()->SetOriginHasExtendedPermissionForTesting(kTestOrigin);
+
+ // Sets up file paths to be the test targets.
+ const auto file_path_info1 =
+ PathInfo(kTestPathInfo.path.AppendASCII("test_file1.txt"));
+ const auto file_path_info2 =
+ PathInfo(kTestPathInfo.path.AppendASCII("test_file2.txt"));
+
+ // Grant read and write permission to the file paths.
+ EXPECT_EQ(permission_context()
+ ->GetReadPermissionGrant(kTestOrigin, file_path_info1,
+ HandleType::kFile, UserAction::kSave)
+ ->GetStatus(),
+ PermissionStatus::GRANTED);
+ EXPECT_EQ(permission_context()
+ ->GetReadPermissionGrant(kTestOrigin, file_path_info2,
+ HandleType::kFile, UserAction::kSave)
+ ->GetStatus(),
+ PermissionStatus::GRANTED);
+
+ // 1. Revoke the read permission for the file paths by calling NotifyEntryRemoved.
+ // This adds the paths to downgraded_read_paths.
+ permission_context()->NotifyEntryRemoved(kTestOrigin, file_path_info1);
+ permission_context()->NotifyEntryRemoved(kTestOrigin, file_path_info2);
+
+ // Verify the paths are added to downgraded_read_paths.
+ EXPECT_TRUE(permission_context()->IsPathInDowngradedReadPathsForTesting(
+ kTestOrigin, file_path_info1.path));
+ EXPECT_TRUE(permission_context()->IsPathInDowngradedReadPathsForTesting(
+ kTestOrigin, file_path_info2.path));
+
+ // 2. Revoke active grant for a specific path.
+ permission_context()->RevokeActiveGrantsForTesting(kTestOrigin,
+ file_path_info1.path);
+
+ // Verify file_path_info1 is removed from downgraded_read_paths.
+ EXPECT_FALSE(permission_context()->IsPathInDowngradedReadPathsForTesting(
+ kTestOrigin, file_path_info1.path));
+ // Verify file_path_info2 is STILL in downgraded_read_paths.
+ EXPECT_TRUE(permission_context()->IsPathInDowngradedReadPathsForTesting(
+ kTestOrigin, file_path_info2.path));
+}
+
+// Regression test for crbug.com/499078161.
+TEST_F(ChromeFileSystemAccessPermissionContextTest,
+ RevokeAllActiveGrants_ClearsDowngradedReadPaths) {
+ base::test::ScopedFeatureList feature_list;
+ feature_list.InitAndEnableFeature(
+ blink::features::kFileSystemAccessRevokeReadOnRemove);
+ FileSystemAccessPermissionRequestManager::FromWebContents(web_contents())
+ ->set_auto_response_for_test(PermissionAction::GRANTED);
+ permission_context()->SetOriginHasExtendedPermissionForTesting(kTestOrigin);
+
+ // Sets up a file path to be the test target.
+ const auto file_path_info =
+ PathInfo(kTestPathInfo.path.AppendASCII("test_file.txt"));
+
+ // Grant read and write permission to the file path.
+ EXPECT_EQ(permission_context()
+ ->GetReadPermissionGrant(kTestOrigin, file_path_info,
+ HandleType::kFile, UserAction::kSave)
+ ->GetStatus(),
+ PermissionStatus::GRANTED);
+
+ // 1. Revoke the read permission for the file path by calling NotifyEntryRemoved.
+ // This adds the path to downgraded_read_paths.
+ permission_context()->NotifyEntryRemoved(kTestOrigin, file_path_info);
+
+ // Verify the path is added to downgraded_read_paths.
+ EXPECT_TRUE(permission_context()->IsPathInDowngradedReadPathsForTesting(
+ kTestOrigin, file_path_info.path));
+
+ // 2. Revoke all active grants. This should clear downgraded_read_paths for all origins.
+ permission_context()->RevokeAllActiveGrants();
+
+ // Verify the path is removed from downgraded_read_paths.
+ EXPECT_FALSE(permission_context()->IsPathInDowngradedReadPathsForTesting(
+ kTestOrigin, file_path_info.path));
+}
#endif // !BUILDFLAG(IS_ANDROID)
TEST_F(ChromeFileSystemAccessPermissionContextTest,
Original Bug Report
Permission escalation from write-only to read via stale downgraded_read_paths
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 without the security team.
Overview: ChromeFileSystemAccessPermissionContext fails to clear the downgraded_read_paths set when a user explicitly revokes File System Access permissions. An attacker can exploit this stale state to silently escalate a subsequent write-only permission prompt into full read-write access, bypassing the user’s revocation intent.
Affected files:
chrome/browser/file_system_access/chrome_file_system_access_permission_context.cc
Estimated timestamp from git blame: 2024-08-14
The ‘Revoke Read on Remove’ feature in the File System Access API downgrades a file’s read permission to ASK when an origin calls FileSystemHandle.remove(). To facilitate automatic restoration of access if the file is recreated by the origin, the API tracks these paths in OriginState::downgraded_read_paths.
However, there is a state leak in ChromeFileSystemAccessPermissionContext::RevokeActiveGrants. When a user explicitly revokes an origin’s permissions via the browser UI, the function correctly iterates over read_grants and write_grants to set their status to ASK, but it entirely fails to clear the downgraded_read_paths set.
Because this set persists until the profile is destroyed, an attacker can use a stale entry to trick the browser into silently restoring read access. If the attacker obtains write access to the path (e.g., via a write-only permission prompt), creating a swap file and closing the writer triggers MaybeRestoreReadPermission, which sees the stale entry and elevates the read grant from ASK to GRANTED without user consent.
Potential Attack Scenario
- An attacker website tricks a user into granting read and write access to a file (e.g.,
/tmp/data.txt) and stores theFileSystemHandlein IndexedDB. - The attacker calls
await handle.remove(). This deletes the file, downgrades the read grant toASK, and inserts the path into thedowngraded_read_pathsset. - The user explicitly revokes the origin’s File System Access permissions via the Chrome UI. This sets the write grant to
ASK, but thedowngraded_read_pathsset is not cleared. - A sensitive file is subsequently created by the user or OS at the exact same location (
/tmp/data.txt). - The attacker reloads the page and retrieves the
FileSystemHandlefrom IndexedDB. - The attacker calls
const w = await handle.createWritable({keepExistingData: true}). Because the write grant was revoked, the user is shown a prompt requesting WRITE-ONLY access. Believing the site only wants to save data, the user approves. - The attacker immediately calls
await w.close(). The backend creates a swap file via a browser-privileged copy (which requires no origin read access check) and replaces the original file. - The file replacement triggers
MaybeNotifyEntryModified, which callsMaybeRestoreReadPermission. MaybeRestoreReadPermissionfinds the path in the staledowngraded_read_pathsset and erroneously elevates the origin’s read permission fromASKtoGRANTED.- The attacker can now read the sensitive file content using
await (await handle.getFile()).text(), fully bypassing the user’s previous revocation.
(Note: Our tooling agent cannot run code, so these are potential steps derived from static code analysis.)
Suggested Fix
Update ChromeFileSystemAccessPermissionContext::RevokeActiveGrants to clear the downgraded_read_paths set when revoking permissions.
// In chrome/browser/file_system_access/chrome_file_system_access_permission_context.cc
bool ChromeFileSystemAccessPermissionContext::RevokeActiveGrants(
const url::Origin& origin,
base::FilePath file_path) {
// ... existing code ...
if (origin_it != active_permissions_map_.end()) {
OriginState& origin_state = origin_it->second;
// Clear the downgraded paths to prevent unauthorized read restoration
if (file_path.empty()) {
origin_state.downgraded_read_paths.clear();
} else {
origin_state.downgraded_read_paths.erase(file_path);
}
// ... rest of the method ...
}
}
Evaluated with Chrome root at commit: ff3d2b74fa39431785bd60e51463b08fcc71ee33
Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; 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.