Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient validation of untrusted input in FileSystem
DescriptionInsufficient validation of untrusted input in FileSystem
ComponentFileSystem
Bug ClassLogic Error
Tracker499917177
Fix commit12f2a15d6550 (chromium/src) +125/-1
CISA KEVNot listed
CreditedEran Rom of Palo Alto Networks
Disclosed2026-06-02

Changed Functions

FunctionChangeNotes
FileSystemAccessObserverCrossOriginTokenBypassTest
content/browser/file_system_access/file_system_access_observer_browsertest.cc
modified
if
content/browser/file_system_access/file_system_access_observer_host.cc
modified

Files Changed

  • content/browser/file_system_access/file_system_access_observer_browsertest.cc
  • content/browser/file_system_access/file_system_access_observer_host.cc
From 12f2a15d6550acdb736b12c5a394002c1dcd1e80 Mon Sep 17 00:00:00 2001
From: Fergal Daly <[email protected]>
Date: Wed, 22 Apr 2026 21:16:34 -0700
Subject: [PATCH] Fix a cross-site leak of file-monitoring capability.

If site-A has permission to monitor a file or directory then a
compromised renderer on site-B can also monitor it.

The fix is to check that the site associated with the final resolved
token is the current site before granting access.

Fixed: 499917177
Change-Id: I815a1a3dbf382b71758565b8ca035a3da5547eb0
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7784855
Commit-Queue: Fergal Daly <[email protected]>
Reviewed-by: Ming-Ying Chung <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1619316}
---

diff --git a/content/browser/file_system_access/file_system_access_observer_browsertest.cc b/content/browser/file_system_access/file_system_access_observer_browsertest.cc
index be7bca9..1215895 100644
--- a/content/browser/file_system_access/file_system_access_observer_browsertest.cc
+++ b/content/browser/file_system_access/file_system_access_observer_browsertest.cc
@@ -8,10 +8,14 @@
 #include "base/files/file_util.h"
 #include "base/files/scoped_temp_dir.h"
 #include "base/json/values_util.h"
+#include "base/test/bind.h"
 #include "base/test/metrics/histogram_tester.h"
 #include "base/test/test_timeouts.h"
 #include "base/win/windows_version.h"
 #include "build/buildflag.h"
+#include "content/browser/file_system_access/file_system_access_directory_handle_impl.h"
+#include "content/browser/file_system_access/file_system_access_manager_impl.h"
+#include "content/browser/file_system_access/fixed_file_system_access_permission_grant.h"
 #include "content/browser/web_contents/web_contents_impl.h"
 #include "content/public/browser/render_frame_host.h"
 #include "content/public/common/content_switches.h"
@@ -22,6 +26,7 @@
 #include "content/public/test/content_browser_test_utils.h"
 #include "content/public/test/file_system_chooser_test_helpers.h"
 #include "content/shell/browser/shell.h"
+#include "net/dns/mock_host_resolver.h"
 #include "net/test/embedded_test_server/embedded_test_server.h"
 #include "testing/gtest/include/gtest/gtest.h"
 
@@ -1467,4 +1472,122 @@
 #endif  // !BUILDFLAG(IS_ANDROID) && !BUILDFLAG(IS_IOS) &&
         // !BUILDFLAG(IS_FUCHSIA)
 
+using HandleType = FileSystemAccessPermissionContext::HandleType;
+
+class FileSystemAccessObserverCrossOriginTokenBypassTest
+    : public FileSystemAccessObserverBrowserTestBase {
+ public:
+  void SetUpOnMainThread() override {
+    host_resolver()->AddRule("*", "127.0.0.1");
+    FileSystemAccessObserverBrowserTestBase::SetUpOnMainThread();
+  }
+
+  FileSystemAccessManagerImpl* GetManager() {
+    auto* partition =
+        static_cast<StoragePartitionImpl*>(shell()
+                                               ->web_contents()
+                                               ->GetBrowserContext()
+                                               ->GetDefaultStoragePartition());
+    return partition->GetFileSystemAccessManager();
+  }
+
+  GURL GetURL(std::string_view relative_url) {
+    return embedded_test_server()->GetURL(relative_url);
+  }
+
+  GURL GetURL(std::string_view hostname, std::string_view relative_url) {
+    return embedded_test_server()->GetURL(hostname, relative_url);
+  }
+};
+
+// Checks that tokens from an unexpected origin cannot be used to create an
+// observer. https://crbug.com/499917177
+IN_PROC_BROWSER_TEST_F(FileSystemAccessObserverCrossOriginTokenBypassTest,
+                       ObserveRefusesCrossOriginToken) {
+  base::FilePath dir_path;
+  {
+    base::ScopedAllowBlockingForTesting allow_blocking;
+    ASSERT_TRUE(base::CreateTemporaryDirInDir(
+        temp_dir_.GetPath(), FILE_PATH_LITERAL("victim"), &dir_path));
+  }
+
+  // Victim site.
+  GURL url_victim = GetURL("a.com", "/title1.html");
+  const url::Origin origin_victim = url::Origin::Create(url_victim);
+  const blink::StorageKey key_victim =
+      blink::StorageKey::CreateFirstParty(origin_victim);
+  // Attacker site.
+  GURL url_attacker = GetURL("b.com", "/title1.html");
+  const url::Origin origin_attacker = url::Origin::Create(url_attacker);
+  const blink::StorageKey key_attacker =
+      blink::StorageKey::CreateFirstParty(origin_attacker);
+  ASSERT_NE(origin_victim, origin_attacker);
+
+  // Navigate to get a valid RFH (used for binding contexts).
+  ASSERT_TRUE(NavigateToURL(shell(), url_victim));
+  RenderFrameHost* rfh = shell()->web_contents()->GetPrimaryMainFrame();
+  ASSERT_TRUE(rfh);
+
+  auto* manager = GetManager();
+  ASSERT_TRUE(manager);
+
+  const storage::FileSystemURL dir_url =
+      manager->CreateFileSystemURLFromPath(PathInfo(dir_path));
+
+  auto read_grant = base::MakeRefCounted<FixedFileSystemAccessPermissionGrant>(
+      FixedFileSystemAccessPermissionGrant::PermissionStatus::GRANTED,
+      PathInfo(dir_path));
+  auto write_grant = base::MakeRefCounted<FixedFileSystemAccessPermissionGrant>(
+      FixedFileSystemAccessPermissionGrant::PermissionStatus::GRANTED,
+      PathInfo(dir_path));
+  FileSystemAccessManagerImpl::SharedHandleState handle_state(read_grant,
+                                                              write_grant);
+
+  // Create a directory handle owned by origin A.
+  FileSystemAccessManagerImpl::BindingContext victim_context(
+      key_victim, url_victim, rfh->GetGlobalId());
+  auto dir_handle = std::make_unique<FileSystemAccessDirectoryHandleImpl>(
+      manager, victim_context, dir_url, handle_state);
+
+  // Create a registered transfer token from the directory handle.
+  // Using CreateTransferToken ensures the token is inserted into the
+  // manager's transfer_tokens_ map so that ResolveTransferToken can find it.
+  mojo::PendingRemote<blink::mojom::FileSystemAccessTransferToken> token_remote;
+  manager->CreateTransferToken(*dir_handle,
+                               token_remote.InitWithNewPipeAndPassReceiver());
+
+  // Create a context with B's key but A's URL.
+  FileSystemAccessManagerImpl::BindingContext attacker_context(
+      key_attacker, url_victim, rfh->GetGlobalId());
+
+  // Bind an ObserverHost with origin B's context.
+  mojo::Remote<blink::mojom::FileSystemAccessObserverHost> observer_host;
+  manager->watcher_manager().BindObserverHost(
+      attacker_context, observer_host.BindNewPipeAndPassReceiver());
+
+  // --- The exploit: Observe() with A's token from B's host ---
+  base::RunLoop run_loop;
+  blink::mojom::FileSystemAccessStatus observe_status;
+  observer_host->Observe(
+      std::move(token_remote), /*is_recursive=*/true,
+      base::BindLambdaForTesting(
+          [&](blink::mojom::FileSystemAccessErrorPtr result,
+              mojo::PendingReceiver<blink::mojom::FileSystemAccessObserver>
+                  receiver) {
+            observe_status = result->status;
+            run_loop.Quit();
+          }));
+  run_loop.Run();
+
+  // THE BUG: Observe() succeeds despite origin mismatch.
+  //
+  // DidResolveTransferTokenToObserve does not call IsValidTransferToken
+  // to check the token origin against the binding context origin.
+  // Compare with DidResolveTransferTokenForFileHandle (manager_impl.cc:1442)
+  // and DidResolveTransferTokenForDirectoryHandle (manager_impl.cc:1467)
+  // which both call IsValidTransferToken and reject cross-origin tokens.
+  ASSERT_EQ(observe_status,
+            blink::mojom::FileSystemAccessStatus::kInvalidArgument);
+}
+
 }  // namespace content
diff --git a/content/browser/file_system_access/file_system_access_observer_host.cc b/content/browser/file_system_access/file_system_access_observer_host.cc
index abd4225..a3afbb6 100644
--- a/content/browser/file_system_access/file_system_access_observer_host.cc
+++ b/content/browser/file_system_access/file_system_access_observer_host.cc
@@ -75,7 +75,8 @@
     FileSystemAccessTransferTokenImpl* resolved_token) {
   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
 
-  if (!resolved_token) {
+  if (!resolved_token ||
+      resolved_token->origin() != binding_context_.storage_key.origin()) {
     std::move(callback).Run(
         file_system_access_error::FromStatus(
             blink::mojom::FileSystemAccessStatus::kInvalidArgument),
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/content/browser/file_system_access/file_system_access_observer_browsertest.cc b/content/browser/file_system_access/file_system_access_observer_browsertest.cc
index be7bca9..1215895 100644
--- a/content/browser/file_system_access/file_system_access_observer_browsertest.cc
+++ b/content/browser/file_system_access/file_system_access_observer_browsertest.cc
@@ -8,10 +8,14 @@
 #include "base/files/file_util.h"
 #include "base/files/scoped_temp_dir.h"
 #include "base/json/values_util.h"
+#include "base/test/bind.h"
 #include "base/test/metrics/histogram_tester.h"
 #include "base/test/test_timeouts.h"
 #include "base/win/windows_version.h"
 #include "build/buildflag.h"
+#include "content/browser/file_system_access/file_system_access_directory_handle_impl.h"
+#include "content/browser/file_system_access/file_system_access_manager_impl.h"
+#include "content/browser/file_system_access/fixed_file_system_access_permission_grant.h"
 #include "content/browser/web_contents/web_contents_impl.h"
 #include "content/public/browser/render_frame_host.h"
 #include "content/public/common/content_switches.h"
@@ -22,6 +26,7 @@
 #include "content/public/test/content_browser_test_utils.h"
 #include "content/public/test/file_system_chooser_test_helpers.h"
 #include "content/shell/browser/shell.h"
+#include "net/dns/mock_host_resolver.h"
 #include "net/test/embedded_test_server/embedded_test_server.h"
 #include "testing/gtest/include/gtest/gtest.h"
 
@@ -1467,4 +1472,122 @@
 #endif  // !BUILDFLAG(IS_ANDROID) && !BUILDFLAG(IS_IOS) &&
         // !BUILDFLAG(IS_FUCHSIA)
 
+using HandleType = FileSystemAccessPermissionContext::HandleType;
+
+class FileSystemAccessObserverCrossOriginTokenBypassTest
+    : public FileSystemAccessObserverBrowserTestBase {
+ public:
+  void SetUpOnMainThread() override {
+    host_resolver()->AddRule("*", "127.0.0.1");
+    FileSystemAccessObserverBrowserTestBase::SetUpOnMainThread();
+  }
+
+  FileSystemAccessManagerImpl* GetManager() {
+    auto* partition =
+        static_cast<StoragePartitionImpl*>(shell()
+                                               ->web_contents()
+                                               ->GetBrowserContext()
+                                               ->GetDefaultStoragePartition());
+    return partition->GetFileSystemAccessManager();
+  }
+
+  GURL GetURL(std::string_view relative_url) {
+    return embedded_test_server()->GetURL(relative_url);
+  }
+
+  GURL GetURL(std::string_view hostname, std::string_view relative_url) {
+    return embedded_test_server()->GetURL(hostname, relative_url);
+  }
+};
+
+// Checks that tokens from an unexpected origin cannot be used to create an
+// observer. https://crbug.com/499917177
+IN_PROC_BROWSER_TEST_F(FileSystemAccessObserverCrossOriginTokenBypassTest,
+                       ObserveRefusesCrossOriginToken) {
+  base::FilePath dir_path;
+  {
+    base::ScopedAllowBlockingForTesting allow_blocking;
+    ASSERT_TRUE(base::CreateTemporaryDirInDir(
+        temp_dir_.GetPath(), FILE_PATH_LITERAL("victim"), &dir_path));
+  }
+
+  // Victim site.
+  GURL url_victim = GetURL("a.com", "/title1.html");
+  const url::Origin origin_victim = url::Origin::Create(url_victim);
+  const blink::StorageKey key_victim =
+      blink::StorageKey::CreateFirstParty(origin_victim);
+  // Attacker site.
+  GURL url_attacker = GetURL("b.com", "/title1.html");
+  const url::Origin origin_attacker = url::Origin::Create(url_attacker);
+  const blink::StorageKey key_attacker =
+      blink::StorageKey::CreateFirstParty(origin_attacker);
+  ASSERT_NE(origin_victim, origin_attacker);
+
+  // Navigate to get a valid RFH (used for binding contexts).
+  ASSERT_TRUE(NavigateToURL(shell(), url_victim));
+  RenderFrameHost* rfh = shell()->web_contents()->GetPrimaryMainFrame();
+  ASSERT_TRUE(rfh);
+
+  auto* manager = GetManager();
+  ASSERT_TRUE(manager);
+
+  const storage::FileSystemURL dir_url =
+      manager->CreateFileSystemURLFromPath(PathInfo(dir_path));
+
+  auto read_grant = base::MakeRefCounted<FixedFileSystemAccessPermissionGrant>(
+      FixedFileSystemAccessPermissionGrant::PermissionStatus::GRANTED,
+      PathInfo(dir_path));
+  auto write_grant = base::MakeRefCounted<FixedFileSystemAccessPermissionGrant>(
+      FixedFileSystemAccessPermissionGrant::PermissionStatus::GRANTED,
+      PathInfo(dir_path));
+  FileSystemAccessManagerImpl::SharedHandleState handle_state(read_grant,
+                                                              write_grant);
+
+  // Create a directory handle owned by origin A.
+  FileSystemAccessManagerImpl::BindingContext victim_context(
+      key_victim, url_victim, rfh->GetGlobalId());
+  auto dir_handle = std::make_unique<FileSystemAccessDirectoryHandleImpl>(
+      manager, victim_context, dir_url, handle_state);
+
+  // Create a registered transfer token from the directory handle.
+  // Using CreateTransferToken ensures the token is inserted into the
+  // manager's transfer_tokens_ map so that ResolveTransferToken can find it.
+  mojo::PendingRemote<blink::mojom::FileSystemAccessTransferToken> token_remote;
+  manager->CreateTransferToken(*dir_handle,
+                               token_remote.InitWithNewPipeAndPassReceiver());
+
+  // Create a context with B's key but A's URL.
+  FileSystemAccessManagerImpl::BindingContext attacker_context(
+      key_attacker, url_victim, rfh->GetGlobalId());
+
+  // Bind an ObserverHost with origin B's context.
+  mojo::Remote<blink::mojom::FileSystemAccessObserverHost> observer_host;
+  manager->watcher_manager().BindObserverHost(
+      attacker_context, observer_host.BindNewPipeAndPassReceiver());
+
+  // --- The exploit: Observe() with A's token from B's host ---
+  base::RunLoop run_loop;
+  blink::mojom::FileSystemAccessStatus observe_status;
+  observer_host->Observe(
+      std::move(token_remote), /*is_recursive=*/true,
+      base::BindLambdaForTesting(
+          [&](blink::mojom::FileSystemAccessErrorPtr result,
+              mojo::PendingReceiver<blink::mojom::FileSystemAccessObserver>
+                  receiver) {
+            observe_status = result->status;
+            run_loop.Quit();
+          }));
+  run_loop.Run();
+
+  // THE BUG: Observe() succeeds despite origin mismatch.
+  //
+  // DidResolveTransferTokenToObserve does not call IsValidTransferToken
+  // to check the token origin against the binding context origin.
+  // Compare with DidResolveTransferTokenForFileHandle (manager_impl.cc:1442)
+  // and DidResolveTransferTokenForDirectoryHandle (manager_impl.cc:1467)
+  // which both call IsValidTransferToken and reject cross-origin tokens.
+  ASSERT_EQ(observe_status,
+            blink::mojom::FileSystemAccessStatus::kInvalidArgument);
+}
+
 }  // namespace content
Loading diff…

Original Bug Report

reported by [Deleted User]

Missing origin check in FileSystemAccessObserverHost::Observe() allows cross-origin file observation

Steps to reproduce the problem

  1. Apply the attached patch from the root of the Chromium src tree: git apply fsa_observer_xorigin_bypass.patch

  2. Build: autoninja -C out/Default content_browsertests

  3. Run: out/Default/content_browsertests
    –gtest_filter=’ObserverCrossOriginTokenBypass'

  4. Observe the output:

    • “ObserveAcceptsCrossOriginToken” PASSES with log: “VULNERABILITY CONFIRMED: Observe() accepted a transfer token from https://victim.example on an ObserverHost bound to https://attacker.example
    • “ResolveTransferTokenRejectsCrossOriginToken” PASSES, confirming origin validation works on other code paths and is only missing from Observe().

Problem Description

FileSystemAccessObserverHost::DidResolveTransferTokenToObserve() does not validate the transfer token’s origin against the binding context’s origin before using the token.

A compromised renderer for origin B can call Observe() with a transfer token belonging to origin A, gaining the ability to observe file system changes on directories granted exclusively to origin A. The observation is backed by origin A’s permission grants, meaning handles delivered through change notifications carry origin A’s read/write permissions.

Root Cause: In file_system_access_observer_host.cc, DidResolveTransferTokenToObserve() checks:

  1. Whether the resolved token is non-null
  2. Whether the read grant has GRANTED status

But it doesn’t check whether resolved_token->origin() matches binding_context().storage_key.origin().

All other transfer token redemption paths perform this check:

  • DidResolveTransferTokenForFileHandle (manager_impl.cc:1442) calls IsValidTransferToken()
  • DidResolveTransferTokenForDirectoryHandle (manager_impl.cc:1467) calls IsValidTransferToken()

The IsValidTransferToken() function (manager_impl.cc:302) compares token->origin() against the expected origin from the binding context. This check is simply missing from the Observe() path.

Impact:

  • Cross-origin file observation: attacker monitors file changes in directories granted to another origin
  • Permission grant leakage: directory handles created from the token carry victim origin’s read/write grants

Summary

Missing origin check in FileSystemAccessObserverHost::Observe() allows cross-origin file observation

Custom Questions

Reporter credit:

Eran Rom of Palo Alto Networks

Additional Data

Category: Security
Chrome Channel: Canary
Regression: N/A \

View on issue tracker