Overview

Medium
Severity
6.5
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactMissing authorization in FileSystem
DescriptionMissing authorization 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.
Tracker501627201
Fix commitba62faa8f0a6 (chromium/src) +133/-8
CWECWE-862
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-04-11 (per vendor advisory)
Fix landed2026-07-30 (public git)
Disclosed2026-09-08
Patch gap40 days — fix public in source before the advisory

Background

`SyncFileSystemBackend`
the storage::FileSystemBackend implementation backing the chrome.syncFileSystem extension API, registered process-globally and added to every FileSystemContext.
`syncfs` external mount
the named external filesystem this backend registers, reachable through filesystem:.../external/syncfs/... URLs once a caller resolves them.
`ResolveURL()` / `CreateFileSystemOperation()`
the two SyncFileSystemBackend entry points that translate a FileSystemURL into an open filesystem or into an executable file operation, respectively.
`extensions::kExtensionScheme`
the chrome-extension URL scheme; only origins on this scheme legitimately drive the sync filesystem.

Root Cause Analysis

Because SyncFileSystemBackend registers the syncfs external mount globally and is attached to every FileSystemContext, any origin — including ordinary http:///https:// web origins — could construct a filesystem:.../external/syncfs/... URL and reach the backend through ResolveURL() and CreateFileSystemOperation(). Neither entry point checked the caller’s origin scheme, violating the invariant that the syncable filesystem is private to the chrome.syncFileSystem extension API and should only ever be touched by a chrome-extension:// origin.

The fix introduces IsSyncFSAllowedOrigin(), which returns true only when url.origin().scheme() == extensions::kExtensionScheme, and calls it at the very top of both methods. On a disallowed origin ResolveURL() invokes its callback with base::File::FILE_ERROR_SECURITY and returns, and CreateFileSystemOperation() sets *error_code = FILE_ERROR_SECURITY and returns nullptr, both before any delegate or backend state is touched. This restores the intended authorization boundary so that only extension origins can open or operate on the sync filesystem.

Key insight
The core mistake was a missing authorization check: a backend meant to serve only the chrome.syncFileSystem extension API was exposed to every origin on every FileSystemContext, and the fix adds an origin-scheme gate (IsSyncFSAllowedOrigin()) that rejects any non-chrome-extension:// caller with FILE_ERROR_SECURITY before doing any work.

Attack Path

  1. Reach the global backend Because the syncfs external mount is registered process-wide and SyncFileSystemBackend is added to every FileSystemContext, a non-extension origin (e.g. a normal web page) shares access to the backend.
  2. Craft a syncfs URL The attacker constructs a filesystem: URL targeting external/syncfs/... so that resolving it dispatches into SyncFileSystemBackend.
  3. Enter unguarded entry points The URL flows into ResolveURL() and/or CreateFileSystemOperation(), which (pre-patch) never verify the origin scheme and proceed to open the filesystem or build a file operation.
  4. Operate on the sync filesystem With no authorization check, the web origin drives operations against a backend intended only for the extension API, gaining unauthorized access to sync filesystem state (inferred; the patch demonstrates the missing check but not a specific data-disclosure or corruption primitive).

Impact Assessment

An attacker controlling a non-extension origin could invoke the syncable filesystem backend that is meant to be reserved for the chrome.syncFileSystem extension API, constituting a missing-authorization boundary violation in the browser process’s FileSystem code. The only precondition is the ability to construct and resolve a filesystem:.../external/syncfs/... URL from an ordinary origin. Any escalation beyond unauthorized backend access — data disclosure, corruption, or further compromise — is (inferred); the advisory classifies this as a missing-authorization issue and does not report exploitation in the wild.

Changed Functions

FunctionChangeNotes
if
chrome/browser/sync_file_system/local/sync_file_system_backend.cc
modified
LocalFileSyncContextTest
chrome/browser/sync_file_system/local/local_file_sync_context_unittest.cc
modified
TEST_F
chrome/browser/sync_file_system/local/local_file_sync_service_unittest.cc
modified
SyncFileSystemBackendTest
chrome/browser/sync_file_system/local/sync_file_system_backend_unittest.cc
modified
SyncFileSystemBackendSchemeTest
chrome/browser/sync_file_system/local/sync_file_system_backend_unittest.cc
modified
TEST_P
chrome/browser/sync_file_system/local/sync_file_system_backend_unittest.cc
modified

Files Changed

  • chrome/browser/sync_file_system/local/syncable_file_operation_runner_unittest.cc
  • chrome/browser/sync_file_system/local/local_file_sync_service_unittest.cc
  • chrome/browser/sync_file_system/local/sync_file_system_backend.cc
  • chrome/browser/sync_file_system/local/local_file_change_tracker_unittest.cc
  • chrome/browser/sync_file_system/sync_file_system_service_unittest.cc
  • chrome/browser/sync_file_system/local/sync_file_system_backend_unittest.cc
  • chrome/browser/sync_file_system/local/syncable_file_system_unittest.cc
  • chrome/browser/sync_file_system/BUILD.gn
  • chrome/browser/sync_file_system/local/local_file_sync_context_unittest.cc

Audit Directions

  • Globally registered external mounts
    Audit every storage::FileSystemBackend and external mount that is added to all FileSystemContexts but intended for a narrow consumer, and confirm each entry point authorizes the caller’s origin rather than relying on registration scope.
  • Origin-scheme assumptions in FileSystem entry points
    Look for ResolveURL() / CreateFileSystemOperation() and similar backend methods that trust url.type() or CanHandleType() without validating url.origin(), since type-handling checks do not constrain who may call.
  • Extension-only APIs exposed to web origins
    Review features gated conceptually to chrome-extension:// (via a JS API surface) whose underlying backend is reachable from any origin, ensuring the scheme is enforced in the C++ backend, not just at the API layer.
From ba62faa8f0a610f059e0da197180d7dc36066a72 Mon Sep 17 00:00:00 2001
From: Eriko Kurimoto <elkurin@chromium.org>
Date: Wed, 29 Jul 2026 22:34:39 -0700
Subject: [PATCH] SyncFileSystemBackend: restrict to extension origins

SyncFileSystemBackend registers the process-global "syncfs" external
mount and is added to every FileSystemContext, so any origin could
resolve filesystem:.../external/syncfs/... URLs and perform operations
against it. The only legitimate consumer of this backend is the
chrome.syncFileSystem extension API, which always acts on a
chrome-extension:// origin.

Reject non-extension origins in ResolveURL() and
CreateFileSystemOperation() with FILE_ERROR_SECURITY before any backend
state is touched. Existing CannedSyncableFileSystem-based unit tests are
switched to chrome-extension:// origins to match, and
SyncFileSystemBackendTest is added to cover both the allowed and
rejected origin cases.

Bug: 501627201
Change-Id: Ie25cd6c2477e5129c8a06cb72e912bb716851a61
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8029801
Commit-Queue: Eriko Kurimoto <elkurin@chromium.org>
Reviewed-by: Ming-Ying Chung <mych@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1670836}
---
diff --git a/chrome/browser/sync_file_system/local/sync_file_system_backend.cc b/chrome/browser/sync_file_system/local/sync_file_system_backend.cc
index 170e03f..8a4d9c1 100644
--- a/chrome/browser/sync_file_system/local/sync_file_system_backend.cc
+++ b/chrome/browser/sync_file_system/local/sync_file_system_backend.cc
@@ -21,11 +21,13 @@
 #include "chrome/browser/sync_file_system/syncable_file_system_util.h"
 #include "content/public/browser/browser_task_traits.h"
 #include "content/public/browser/browser_thread.h"
+#include "extensions/common/constants.h"
 #include "storage/browser/file_system/file_stream_reader.h"
 #include "storage/browser/file_system/file_stream_writer.h"
 #include "storage/browser/file_system/file_system_context.h"
 #include "storage/browser/file_system/file_system_operation.h"
 #include "storage/common/file_system/file_system_util.h"
+#include "url/origin.h"
 
 using content::BrowserThread;
 
@@ -33,6 +35,12 @@
 
 namespace {
 
+// The syncable filesystem is only exposed via the chrome.syncFileSystem
+// extension API, so only extension-scheme origins are permitted to use it.
+bool IsSyncFSAllowedOrigin(const url::Origin& origin) {
+  return origin.scheme() == extensions::kExtensionScheme;
+}
+
 bool CalledOnUIThread() {
   // Ensure that these methods are called on the UI thread, except for unittests
   // where a UI thread might not have been created.
@@ -86,6 +94,12 @@
                                        ResolveURLCallback callback) {
   DCHECK(CanHandleType(url.type()));
 
+  if (!IsSyncFSAllowedOrigin(url.origin())) {
+    std::move(callback).Run(GURL(), std::string(),
+                            base::File::FILE_ERROR_SECURITY);
+    return;
+  }
+
   if (skip_initialize_syncfs_service_for_testing_) {
     GetDelegate()->OpenFileSystem(
         url.GetBucket(), url.type(), mode, std::move(callback),
@@ -131,6 +145,11 @@
   DCHECK(context);
   DCHECK(error_code);
 
+  if (!IsSyncFSAllowedOrigin(url.origin())) {
+    *error_code = base::File::FILE_ERROR_SECURITY;
+    return nullptr;
+  }
+
   std::unique_ptr<storage::FileSystemOperationContext> operation_context =
       GetDelegate()->CreateFileSystemOperationContext(url, context, error_code);
   if (!operation_context)
diff --git a/chrome/browser/sync_file_system/local/local_file_change_tracker_unittest.cc b/chrome/browser/sync_file_system/local/local_file_change_tracker_unittest.cc
index f5f6f4e3..024508d3 100644
--- a/chrome/browser/sync_file_system/local/local_file_change_tracker_unittest.cc
+++ b/chrome/browser/sync_file_system/local/local_file_change_tracker_unittest.cc
@@ -40,7 +40,7 @@
   LocalFileChangeTrackerTest()
       : task_environment_(content::BrowserTaskEnvironment::IO_MAINLOOP),
         in_memory_env_(leveldb_chrome::NewMemEnv("LocalFileChangeTrackerTest")),
-        file_system_(GURL("http://example.com"),
+        file_system_(GURL("chrome-extension://example"),
                      in_memory_env_.get(),
                      base::SingleThreadTaskRunner::GetCurrentDefault().get(),
                      base::SingleThreadTaskRunner::GetCurrentDefault().get()) {}
diff --git a/chrome/browser/sync_file_system/local/local_file_sync_context_unittest.cc b/chrome/browser/sync_file_system/local/local_file_sync_context_unittest.cc
index c1388928..2335e601 100644
--- a/chrome/browser/sync_file_system/local/local_file_sync_context_unittest.cc
+++ b/chrome/browser/sync_file_system/local/local_file_sync_context_unittest.cc
@@ -57,8 +57,8 @@
 namespace sync_file_system {
 
 namespace {
-const char kOrigin1[] = "http://example.com";
-const char kOrigin2[] = "http://chromium.org";
+const char kOrigin1[] = "chrome-extension://example";
+const char kOrigin2[] = "chrome-extension://anotherexample";
 }  // namespace
 
 class LocalFileSyncContextTest : public testing::Test {
diff --git a/chrome/browser/sync_file_system/local/local_file_sync_service_unittest.cc b/chrome/browser/sync_file_system/local/local_file_sync_service_unittest.cc
index f0275949..7fdf68c 100644
--- a/chrome/browser/sync_file_system/local/local_file_sync_service_unittest.cc
+++ b/chrome/browser/sync_file_system/local/local_file_sync_service_unittest.cc
@@ -54,7 +54,7 @@
 
 namespace {
 
-const char kOrigin[] = "http://example.com";
+const char kOrigin[] = "chrome-extension://example";
 
 void DidPrepareForProcessRemoteChange(const base::Location& where,
                                       base::OnceClosure oncompleted,
@@ -314,7 +314,7 @@
 #endif
 
 TEST_F(LocalFileSyncServiceTest, MAYBE_LocalChangeObserverMultipleContexts) {
-  const char kOrigin2[] = "http://foo";
+  const char kOrigin2[] = "chrome-extension://foo";
   CannedSyncableFileSystem file_system2(
       GURL(kOrigin2), in_memory_env_.get(), content::GetIOThreadTaskRunner({}),
       base::ThreadPool::CreateSingleThreadTaskRunner({base::MayBlock()}));
diff --git a/chrome/browser/sync_file_system/local/sync_file_system_backend_unittest.cc b/chrome/browser/sync_file_system/local/sync_file_system_backend_unittest.cc
new file mode 100644
index 0000000..44a867cc
--- /dev/null
+++ b/chrome/browser/sync_file_system/local/sync_file_system_backend_unittest.cc
@@ -0,0 +1,105 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/sync_file_system/local/sync_file_system_backend.h"
+
+#include <memory>
+
+#include "base/files/file.h"
+#include "base/task/single_thread_task_runner.h"
+#include "chrome/browser/sync_file_system/local/canned_syncable_file_system.h"
+#include "chrome/browser/sync_file_system/syncable_file_system_util.h"
+#include "content/public/test/browser_task_environment.h"
+#include "storage/browser/file_system/file_system_context.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/leveldatabase/leveldb_chrome.h"
+#include "url/gurl.h"
+
+namespace sync_file_system {
+
+class SyncFileSystemBackendTest : public testing::Test {
+ public:
+  SyncFileSystemBackendTest()
+      : in_memory_env_(leveldb_chrome::NewMemEnv("SyncFileSystemBackendTest")) {
+  }
+
+  SyncFileSystemBackendTest(const SyncFileSystemBackendTest&) = delete;
+  SyncFileSystemBackendTest& operator=(const SyncFileSystemBackendTest&) =
+      delete;
+
+ protected:
+  std::unique_ptr<CannedSyncableFileSystem> CreateFileSystem(
+      const GURL& origin) {
+    auto file_system = std::make_unique<CannedSyncableFileSystem>(
+        origin, in_memory_env_.get(),
+        base::SingleThreadTaskRunner::GetCurrentDefault().get(),
+        base::SingleThreadTaskRunner::GetCurrentDefault().get());
+    file_system->SetUp();
+    return file_system;
+  }
+
+  base::File::Error OpenFileSystemForOrigin(const GURL& origin) {
+    auto file_system = CreateFileSystem(origin);
+    base::File::Error result = file_system->OpenFileSystem();
+    file_system->TearDown();
+    return result;
+  }
+
+  base::File::Error CreateFileSystemOperationForOrigin(const GURL& origin) {
+    auto file_system = CreateFileSystem(origin);
+    base::File::Error result = base::File::FILE_OK;
+    file_system->backend()->CreateFileSystemOperation(
+        storage::OperationType::kCreateFile,
+        CreateSyncableFileSystemURL(origin, base::FilePath()),
+        file_system->file_system_context(), &result);
+    file_system->TearDown();
+    return result;
+  }
+
+  void TearDown() override { RevokeSyncableFileSystem(); }
+
+  content::BrowserTaskEnvironment task_environment_;
+  std::unique_ptr<leveldb::Env> in_memory_env_;
+};
+
+struct SchemeTestParam {
+  const char* scheme_name;
+  const char* url_string;
+  base::File::Error expected_error;
+};
+
+class SyncFileSystemBackendSchemeTest
+    : public SyncFileSystemBackendTest,
+      public ::testing::WithParamInterface<SchemeTestParam> {};
+
+TEST_P(SyncFileSystemBackendSchemeTest, OpenFileSystem) {
+  const SchemeTestParam& param = GetParam();
+  EXPECT_EQ(param.expected_error,
+            OpenFileSystemForOrigin(GURL(param.url_string)));
+}
+
+TEST_P(SyncFileSystemBackendSchemeTest, CreateFileSystemOperation) {
+  const SchemeTestParam& param = GetParam();
+  EXPECT_EQ(param.expected_error,
+            CreateFileSystemOperationForOrigin(GURL(param.url_string)));
+}
+
+const SchemeTestParam kSchemeTestParams[] = {
+    {"chrome_extension", "chrome-extension://example/", base::File::FILE_OK},
+    {"http", "http://example.com/", base::File::FILE_ERROR_SECURITY},
+    {"https", "https://example.com/", base::File::FILE_ERROR_SECURITY},
+    {"file", "file:///foo/bar", base::File::FILE_ERROR_SECURITY},
+    {"chrome", "chrome://settings/", base::File::FILE_ERROR_SECURITY},
+    {"ftp", "ftp://example.com/", base::File::FILE_ERROR_SECURITY},
+};
+
+INSTANTIATE_TEST_SUITE_P(
+    SyncFileSystemBackend,
+    SyncFileSystemBackendSchemeTest,
+    ::testing::ValuesIn(kSchemeTestParams),
+    [](const ::testing::TestParamInfo<SchemeTestParam>& info) {
+      return info.param.scheme_name;
+    });
+
+}  // namespace sync_file_system
diff --git a/chrome/browser/sync_file_system/local/syncable_file_operation_runner_unittest.cc b/chrome/browser/sync_file_system/local/syncable_file_operation_runner_unittest.cc
index faed839..2e8fa9a 100644
--- a/chrome/browser/sync_file_system/local/syncable_file_operation_runner_unittest.cc
+++ b/chrome/browser/sync_file_system/local/syncable_file_operation_runner_unittest.cc
@@ -59,7 +59,7 @@
       : task_environment_(content::BrowserTaskEnvironment::IO_MAINLOOP),
         in_memory_env_(
             leveldb_chrome::NewMemEnv("SyncableFileOperationRunnerTest")),
-        file_system_(GURL("http://example.com"),
+        file_system_(GURL("chrome-extension://example"),
                      in_memory_env_.get(),
                      base::SingleThreadTaskRunner::GetCurrentDefault().get(),
                      base::SingleThreadTaskRunner::GetCurrentDefault().get()),
diff --git a/chrome/browser/sync_file_system/local/syncable_file_system_unittest.cc b/chrome/browser/sync_file_system/local/syncable_file_system_unittest.cc
index 9d73af03..b43fd0c 100644
--- a/chrome/browser/sync_file_system/local/syncable_file_system_unittest.cc
+++ b/chrome/browser/sync_file_system/local/syncable_file_system_unittest.cc
@@ -40,7 +40,7 @@
  public:
   SyncableFileSystemTest()
       : in_memory_env_(leveldb_chrome::NewMemEnv("SyncableFileSystemTest")),
-        file_system_(GURL("http://example.com/"),
+        file_system_(GURL("chrome-extension://example/"),
                      in_memory_env_.get(),
                      base::SingleThreadTaskRunner::GetCurrentDefault().get(),
                      base::SingleThreadTaskRunner::GetCurrentDefault().get()) {}
diff --git a/chrome/browser/sync_file_system/sync_file_system_service_unittest.cc b/chrome/browser/sync_file_system/sync_file_system_service_unittest.cc
index 8c4741d..7810f7f4 100644
--- a/chrome/browser/sync_file_system/sync_file_system_service_unittest.cc
+++ b/chrome/browser/sync_file_system/sync_file_system_service_unittest.cc
@@ -52,7 +52,7 @@
 
 namespace {
 
-const char kOrigin[] = "http://example.com";
+const char kOrigin[] = "chrome-extension://example";
 
 template <typename R> struct AssignTrait {
   typedef const R& ArgumentType;
diff --git a/chrome/browser/sync_file_system/BUILD.gn b/chrome/browser/sync_file_system/BUILD.gn
index 23e6934..12932c05 100644
--- a/chrome/browser/sync_file_system/BUILD.gn
+++ b/chrome/browser/sync_file_system/BUILD.gn
@@ -220,6 +220,7 @@
     "local/local_file_sync_status_unittest.cc",
     "local/mock_sync_status_observer.cc",
     "local/mock_sync_status_observer.h",
+    "local/sync_file_system_backend_unittest.cc",
     "local/syncable_file_operation_runner_unittest.cc",
     "local/syncable_file_system_unittest.cc",
     "logger_unittest.cc",
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/chrome/browser/sync_file_system/local/local_file_change_tracker_unittest.cc b/chrome/browser/sync_file_system/local/local_file_change_tracker_unittest.cc
index f5f6f4e3..024508d3 100644
--- a/chrome/browser/sync_file_system/local/local_file_change_tracker_unittest.cc
+++ b/chrome/browser/sync_file_system/local/local_file_change_tracker_unittest.cc
@@ -40,7 +40,7 @@
   LocalFileChangeTrackerTest()
       : task_environment_(content::BrowserTaskEnvironment::IO_MAINLOOP),
         in_memory_env_(leveldb_chrome::NewMemEnv("LocalFileChangeTrackerTest")),
-        file_system_(GURL("http://example.com"),
+        file_system_(GURL("chrome-extension://example"),
                      in_memory_env_.get(),
                      base::SingleThreadTaskRunner::GetCurrentDefault().get(),
                      base::SingleThreadTaskRunner::GetCurrentDefault().get()) {}
diff --git a/chrome/browser/sync_file_system/local/local_file_sync_context_unittest.cc b/chrome/browser/sync_file_system/local/local_file_sync_context_unittest.cc
index c1388928..2335e601 100644
--- a/chrome/browser/sync_file_system/local/local_file_sync_context_unittest.cc
+++ b/chrome/browser/sync_file_system/local/local_file_sync_context_unittest.cc
@@ -57,8 +57,8 @@
 namespace sync_file_system {
 
 namespace {
-const char kOrigin1[] = "http://example.com";
-const char kOrigin2[] = "http://chromium.org";
+const char kOrigin1[] = "chrome-extension://example";
+const char kOrigin2[] = "chrome-extension://anotherexample";
 }  // namespace
 
 class LocalFileSyncContextTest : public testing::Test {
diff --git a/chrome/browser/sync_file_system/local/local_file_sync_service_unittest.cc b/chrome/browser/sync_file_system/local/local_file_sync_service_unittest.cc
index f0275949..7fdf68c 100644
--- a/chrome/browser/sync_file_system/local/local_file_sync_service_unittest.cc
+++ b/chrome/browser/sync_file_system/local/local_file_sync_service_unittest.cc
@@ -54,7 +54,7 @@
 
 namespace {
 
-const char kOrigin[] = "http://example.com";
+const char kOrigin[] = "chrome-extension://example";
 
 void DidPrepareForProcessRemoteChange(const base::Location& where,
                                       base::OnceClosure oncompleted,
@@ -314,7 +314,7 @@
 #endif
 
 TEST_F(LocalFileSyncServiceTest, MAYBE_LocalChangeObserverMultipleContexts) {
-  const char kOrigin2[] = "http://foo";
+  const char kOrigin2[] = "chrome-extension://foo";
   CannedSyncableFileSystem file_system2(
       GURL(kOrigin2), in_memory_env_.get(), content::GetIOThreadTaskRunner({}),
       base::ThreadPool::CreateSingleThreadTaskRunner({base::MayBlock()}));
diff --git a/chrome/browser/sync_file_system/local/sync_file_system_backend_unittest.cc b/chrome/browser/sync_file_system/local/sync_file_system_backend_unittest.cc
new file mode 100644
index 0000000..44a867cc
--- /dev/null
+++ b/chrome/browser/sync_file_system/local/sync_file_system_backend_unittest.cc
@@ -0,0 +1,105 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/sync_file_system/local/sync_file_system_backend.h"
+
+#include <memory>
+
+#include "base/files/file.h"
+#include "base/task/single_thread_task_runner.h"
+#include "chrome/browser/sync_file_system/local/canned_syncable_file_system.h"
+#include "chrome/browser/sync_file_system/syncable_file_system_util.h"
+#include "content/public/test/browser_task_environment.h"
+#include "storage/browser/file_system/file_system_context.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/leveldatabase/leveldb_chrome.h"
+#include "url/gurl.h"
+
+namespace sync_file_system {
+
+class SyncFileSystemBackendTest : public testing::Test {
+ public:
+  SyncFileSystemBackendTest()
+      : in_memory_env_(leveldb_chrome::NewMemEnv("SyncFileSystemBackendTest")) {
+  }
+
+  SyncFileSystemBackendTest(const SyncFileSystemBackendTest&) = delete;
+  SyncFileSystemBackendTest& operator=(const SyncFileSystemBackendTest&) =
+      delete;
+
+ protected:
+  std::unique_ptr<CannedSyncableFileSystem> CreateFileSystem(
+      const GURL& origin) {
+    auto file_system = std::make_unique<CannedSyncableFileSystem>(
+        origin, in_memory_env_.get(),
+        base::SingleThreadTaskRunner::GetCurrentDefault().get(),
+        base::SingleThreadTaskRunner::GetCurrentDefault().get());
+    file_system->SetUp();
+    return file_system;
+  }
+
+  base::File::Error OpenFileSystemForOrigin(const GURL& origin) {
+    auto file_system = CreateFileSystem(origin);
+    base::File::Error result = file_system->OpenFileSystem();
+    file_system->TearDown();
+    return result;
+  }
+
+  base::File::Error CreateFileSystemOperationForOrigin(const GURL& origin) {
+    auto file_system = CreateFileSystem(origin);
+    base::File::Error result = base::File::FILE_OK;
+    file_system->backend()->CreateFileSystemOperation(
+        storage::OperationType::kCreateFile,
+        CreateSyncableFileSystemURL(origin, base::FilePath()),
+        file_system->file_system_context(), &result);
+    file_system->TearDown();
+    return result;
+  }
+
+  void TearDown() override { RevokeSyncableFileSystem(); }
+
+  content::BrowserTaskEnvironment task_environment_;
+  std::unique_ptr<leveldb::Env> in_memory_env_;
+};
+
+struct SchemeTestParam {
+  const char* scheme_name;
+  const char* url_string;
+  base::File::Error expected_error;
+};
+
+class SyncFileSystemBackendSchemeTest
+    : public SyncFileSystemBackendTest,
+      public ::testing::WithParamInterface<SchemeTestParam> {};
+
+TEST_P(SyncFileSystemBackendSchemeTest, OpenFileSystem) {
+  const SchemeTestParam& param = GetParam();
+  EXPECT_EQ(param.expected_error,
+            OpenFileSystemForOrigin(GURL(param.url_string)));
+}
+
+TEST_P(SyncFileSystemBackendSchemeTest, CreateFileSystemOperation) {
+  const SchemeTestParam& param = GetParam();
+  EXPECT_EQ(param.expected_error,
+            CreateFileSystemOperationForOrigin(GURL(param.url_string)));
+}
+
+const SchemeTestParam kSchemeTestParams[] = {
+    {"chrome_extension", "chrome-extension://example/", base::File::FILE_OK},
+    {"http", "http://example.com/", base::File::FILE_ERROR_SECURITY},
+    {"https", "https://example.com/", base::File::FILE_ERROR_SECURITY},
+    {"file", "file:///foo/bar", base::File::FILE_ERROR_SECURITY},
+    {"chrome", "chrome://settings/", base::File::FILE_ERROR_SECURITY},
+    {"ftp", "ftp://example.com/", base::File::FILE_ERROR_SECURITY},
+};
+
+INSTANTIATE_TEST_SUITE_P(
+    SyncFileSystemBackend,
+    SyncFileSystemBackendSchemeTest,
+    ::testing::ValuesIn(kSchemeTestParams),
+    [](const ::testing::TestParamInfo<SchemeTestParam>& info) {
+      return info.param.scheme_name;
+    });
+
+}  // namespace sync_file_system
diff --git a/chrome/browser/sync_file_system/local/syncable_file_operation_runner_unittest.cc b/chrome/browser/sync_file_system/local/syncable_file_operation_runner_unittest.cc
index faed839..2e8fa9a 100644
--- a/chrome/browser/sync_file_system/local/syncable_file_operation_runner_unittest.cc
+++ b/chrome/browser/sync_file_system/local/syncable_file_operation_runner_unittest.cc
@@ -59,7 +59,7 @@
       : task_environment_(content::BrowserTaskEnvironment::IO_MAINLOOP),
         in_memory_env_(
             leveldb_chrome::NewMemEnv("SyncableFileOperationRunnerTest")),
-        file_system_(GURL("http://example.com"),
+        file_system_(GURL("chrome-extension://example"),
                      in_memory_env_.get(),
                      base::SingleThreadTaskRunner::GetCurrentDefault().get(),
                      base::SingleThreadTaskRunner::GetCurrentDefault().get()),
diff --git a/chrome/browser/sync_file_system/local/syncable_file_system_unittest.cc b/chrome/browser/sync_file_system/local/syncable_file_system_unittest.cc
index 9d73af03..b43fd0c 100644
--- a/chrome/browser/sync_file_system/local/syncable_file_system_unittest.cc
+++ b/chrome/browser/sync_file_system/local/syncable_file_system_unittest.cc
@@ -40,7 +40,7 @@
  public:
   SyncableFileSystemTest()
       : in_memory_env_(leveldb_chrome::NewMemEnv("SyncableFileSystemTest")),
-        file_system_(GURL("http://example.com/"),
+        file_system_(GURL("chrome-extension://example/"),
                      in_memory_env_.get(),
                      base::SingleThreadTaskRunner::GetCurrentDefault().get(),
                      base::SingleThreadTaskRunner::GetCurrentDefault().get()) {}
diff --git a/chrome/browser/sync_file_system/sync_file_system_service_unittest.cc b/chrome/browser/sync_file_system/sync_file_system_service_unittest.cc
index 8c4741d..7810f7f4 100644
--- a/chrome/browser/sync_file_system/sync_file_system_service_unittest.cc
+++ b/chrome/browser/sync_file_system/sync_file_system_service_unittest.cc
@@ -52,7 +52,7 @@
 
 namespace {
 
-const char kOrigin[] = "http://example.com";
+const char kOrigin[] = "chrome-extension://example";
 
 template <typename R> struct AssignTrait {
   typedef const R& ArgumentType;
Loading diff…

Original Bug Report

reported by vm...@google.com

Unauthorized Google Drive API access via SyncFileSystem permission bypass

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 Chrome Security team.

Overview: A potential vulnerability exists where standard web origins can resolve the global syncfs mount point due to a permission policy flaw. This allows an attacker to bypass extension checks and trigger authenticated Google Drive API writes via the deprecated SyncFileSystem component. Malicious sites could potentially create directories and sync files to a user’s Google Drive without interaction.

Affected files:

  • chrome/browser/sync_file_system/local/sync_file_system_backend.cc
  • chrome/browser/sync_file_system/sync_file_system_service.cc
  • chrome/browser/sync_file_system/drive_backend/sync_worker.cc
  • chrome/browser/sync_file_system/drive_backend/sync_engine.cc
  • chrome/browser/sync_file_system/drive_backend/register_app_task.cc
  • chrome/browser/sync_file_system/syncable_file_system_util.cc

Estimated timestamp from git blame: 2025-09-24

Technical Details

There is a potential vulnerability in the SyncFileSystem component that allows arbitrary web origins to bypass extension permission checks and trigger authenticated Google Drive API calls. The core issue stems from how the global syncfs mount point is registered and authorized.

During startup, SyncFileSystemBackend registers a process-global mount point named syncfs mapped to storage::kFileSystemTypeSyncable (chrome/browser/sync_file_system/syncable_file_system_util.cc:37). In storage::FileSystemContext::GetPermissionPolicy, kFileSystemTypeSyncable is explicitly associated with storage::FILE_PERMISSION_SANDBOX.

When a standard web renderer requests access to this mount point, the security check is handled by ChildProcessSecurityPolicyImpl::HasPermissionsForFileSystemFile. Because the filesystem type is associated with FILE_PERMISSION_SANDBOX, the policy immediately grants access as long as the renderer’s origin matches the URL’s origin. It does not perform the strict, path-based access controls typically applied to non-sandboxed external filesystems. This effectively allows any web origin to resolve filesystem:<origin>/external/syncfs/x.

Once access is granted, the request is routed to SyncFileSystemService::InitializeForApp. This method initializes the local filesystem context but critically fails to verify whether the requesting origin corresponds to a valid, installed Chrome Extension holding the syncFileSystem permission.

While Chrome attempts to disable this deprecated service by default, this mitigation is bypassed. When Chrome Sync is enabled and the APPS data type is active, SyncFileSystemService::OnStateChanged calls remote_service_->SetSyncEnabled(true). This triggers SyncEngine::SetSyncEnabled, which recreates the SyncWorker, effectively reviving the service.

Following initialization, SyncWorker::RegisterOrigin is called with the attacker’s origin. It schedules a RegisterAppTask, which uses the DriveServiceInterface (and the user’s OAuth tokens) to perform an authenticated Google Drive API request (AddNewDirectory) to create a folder named after the attacker’s host.

Impact

A malicious website could potentially cause the browser to perform unauthorized, OAuth-authenticated writes to a signed-in user’s Google Drive (provided Chrome Sync is enabled) without any user interaction or extension privileges.

Potential Reproduction Steps

(Note: These are suggested steps based on code analysis; a live PoC has not been executed by this agent.)

  1. Sign in to Chrome with a Google account and ensure Chrome Sync is enabled.
  2. Navigate to an attacker-controlled HTTPS page (e.g., https://attacker.com).
  3. The page executes the following JavaScript: webkitResolveLocalFileSystemURL('filesystem:' + location.origin + '/external/syncfs/x', function(entry){}, function(err){});
  4. The browser process should proceed to perform authenticated Drive API calls and create a new directory named attacker.com within the hidden ‘Chrome Syncable FileSystem’ root folder in the user’s Drive.

Suggested Fix

  1. Add explicit permission checks: SyncFileSystemService::InitializeForApp must explicitly verify that the requesting origin_url belongs to a valid, installed Chrome App or Extension that holds the syncFileSystem permission.
  2. Review Permission Policy: Re-evaluate whether kFileSystemTypeSyncable should be granted FILE_PERMISSION_SANDBOX, as this allows standard web origins to bypass external mount point restrictions.
  3. Remove Deprecated Code: Given that the SyncFileSystem API is deprecated, the most robust fix would be to completely remove the chrome/browser/sync_file_system/ component and related Drive backend code if it is no longer required.

Evaluated with Chrome root at commit: 096fc8fdbfacf2546485756d03f160a3d04fcc9b


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.

View on issue tracker
Links in the report