CVE-2026-87603
Overview
Background
- `SyncFileSystemBackend`
- the
storage::FileSystemBackendimplementation backing thechrome.syncFileSystemextension API, registered process-globally and added to everyFileSystemContext. - `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
SyncFileSystemBackendentry points that translate aFileSystemURLinto an open filesystem or into an executable file operation, respectively. - `extensions::kExtensionScheme`
- the
chrome-extensionURL 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.
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
- Reach the global backend
Because the
syncfsexternal mount is registered process-wide andSyncFileSystemBackendis added to everyFileSystemContext, a non-extension origin (e.g. a normal web page) shares access to the backend. - Craft a syncfs URL
The attacker constructs a
filesystem:URL targetingexternal/syncfs/...so that resolving it dispatches intoSyncFileSystemBackend. - Enter unguarded entry points
The URL flows into
ResolveURL()and/orCreateFileSystemOperation(), which (pre-patch) never verify the origin scheme and proceed to open the filesystem or build a file operation. - 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
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
| Function | Change | Notes |
|---|---|---|
ifchrome/browser/sync_file_system/local/sync_file_system_backend.cc |
modified | |
LocalFileSyncContextTestchrome/browser/sync_file_system/local/local_file_sync_context_unittest.cc |
modified | |
TEST_Fchrome/browser/sync_file_system/local/local_file_sync_service_unittest.cc |
modified | |
SyncFileSystemBackendTestchrome/browser/sync_file_system/local/sync_file_system_backend_unittest.cc |
modified | |
SyncFileSystemBackendSchemeTestchrome/browser/sync_file_system/local/sync_file_system_backend_unittest.cc |
modified | |
TEST_Pchrome/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.ccchrome/browser/sync_file_system/local/local_file_sync_service_unittest.ccchrome/browser/sync_file_system/local/sync_file_system_backend.ccchrome/browser/sync_file_system/local/local_file_change_tracker_unittest.ccchrome/browser/sync_file_system/sync_file_system_service_unittest.ccchrome/browser/sync_file_system/local/sync_file_system_backend_unittest.ccchrome/browser/sync_file_system/local/syncable_file_system_unittest.ccchrome/browser/sync_file_system/BUILD.gnchrome/browser/sync_file_system/local/local_file_sync_context_unittest.cc
Audit Directions
- Globally registered external mountsAudit every
storage::FileSystemBackendand external mount that is added to allFileSystemContexts 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 pointsLook for
ResolveURL()/CreateFileSystemOperation()and similar backend methods that trusturl.type()orCanHandleType()without validatingurl.origin(), since type-handling checks do not constrain who may call. - Extension-only APIs exposed to web originsReview 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.
Patch
From ba62faa8f0a610f059e0da197180d7dc36066a72 Mon Sep 17 00:00:00 2001 From: Eriko Kurimoto <[email protected]> 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 <[email protected]> Reviewed-by: Ming-Ying Chung <[email protected]> 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",
Regression Test / PoC
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;
Original Bug Report
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.ccchrome/browser/sync_file_system/sync_file_system_service.ccchrome/browser/sync_file_system/drive_backend/sync_worker.ccchrome/browser/sync_file_system/drive_backend/sync_engine.ccchrome/browser/sync_file_system/drive_backend/register_app_task.ccchrome/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.)
- Sign in to Chrome with a Google account and ensure Chrome Sync is enabled.
- Navigate to an attacker-controlled HTTPS page (e.g.,
https://attacker.com). - The page executes the following JavaScript:
webkitResolveLocalFileSystemURL('filesystem:' + location.origin + '/external/syncfs/x', function(entry){}, function(err){}); - The browser process should proceed to perform authenticated Drive API calls and create a new directory named
attacker.comwithin the hidden ‘Chrome Syncable FileSystem’ root folder in the user’s Drive.
Suggested Fix
- Add explicit permission checks:
SyncFileSystemService::InitializeForAppmust explicitly verify that the requestingorigin_urlbelongs to a valid, installed Chrome App or Extension that holds thesyncFileSystempermission. - Review Permission Policy: Re-evaluate whether
kFileSystemTypeSyncableshould be grantedFILE_PERMISSION_SANDBOX, as this allows standard web origins to bypassexternalmount point restrictions. - 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.