Overview

Medium
Severity
9.6
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactIncorrect reference resolution in FileSystem
DescriptionIncorrect reference resolution in FileSystem
ComponentFileSystem
Bug ClassLogic 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.
Tracker497111188
Fix commit9ab0e2155012 (chromium/src) +123/-4
CWECWE-706
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-03-28 (per vendor advisory)
Fix landed2026-08-04 (public git)
Disclosed2026-09-08
Patch gap35 days — fix public in source before the advisory

Background

`FileSystemAccessDirectoryHandleImpl`
the browser-process implementation backing a renderer’s File System Access directory handle, exposing GetFile(), GetDirectory(), and RemoveEntry() over Mojo.
content-URI
an Android content:// reference (detected by url().virtual_path().IsContentUri()) used to address a document/tree instead of a plain filesystem path.
`IsSafePathComponent()`
a validation predicate (implemented via manager()->IsSafePathComponent(url().type(), basename)) that rejects a basename that is not a single safe path component, e.g. "", "..", "../a", "a/b", "a\\b".
`GetChildURL()`
the helper that builds a child storage::FileSystemURL from a basename and is the only place that previously called IsSafePathComponent().

Root Cause Analysis

On Android, GetFile(), GetDirectory(), and RemoveEntry() each contained an early-return branch guarded by if (url().virtual_path().IsContentUri()) that resolved the child document directly (via base::ContentUriGetChildDocumentOrQuery / MIME lookup) and returned before ever reaching GetChildURL(). Because GetChildURL() was the sole caller of IsSafePathComponent(), the content-URI path never validated the caller-supplied basename, so the invariant “every child name is a safe single path component” was not enforced for content-URI-backed handles.

The fix adds an if (!IsSafePathComponent(basename)) check at the top of each content-URI branch that runs callback with FileSystemAccessStatus::kInvalidArgument (“Name is not allowed.”) and returns before any child-resolution work. It also introduces the member helper FileSystemAccessDirectoryHandleImpl::IsSafePathComponent() and refactors GetChildURL() to call it (moving the parent local below the check), and makes manager()/file_system_context() in file_system_access_handle_base.h const so the new const helper can call them.

The fix works because the safety check now runs on the content-URI path before child resolution, matching the check already applied on the non-content-URI path.

Key insight
The single mistake is that the content-URI early-return branches bypassed the only code (GetChildURL()) that ran IsSafePathComponent(), leaving traversal sequences like ".." unvalidated; the fix restores the invariant by validating basename at each branch’s entry point before the early return.

Attack Path

  1. Obtain a content-URI-backed directory handle On Android, a page acquires a File System Access directory handle whose underlying url().virtual_path() is a content-URI (IsContentUri() true), e.g. via a directory picker over a content:// tree.
  2. Invoke a child operation with an unsafe name The renderer calls GetFile(), GetDirectory(), or RemoveEntry() with a basename that is not a safe path component, such as ".." or "../a" (the exact names the added tests reject).
  3. Bypass validation via the early return Pre-patch, the IsContentUri() branch resolved/queried the child document directly and returned before GetChildURL(), so IsSafePathComponent() never ran and the unsafe basename reached child resolution. (inferred) This permits path traversal outside the granted directory on the content-URI, consistent with the advisory’s “Incorrect reference resolution in FileSystem.”
  4. Escalation Any read/write/delete outside the intended directory scope, and any further sandbox or privilege impact, is not demonstrated by the diff. (inferred)

Impact Assessment

An attacker-controlled renderer could pass traversal sequences (e.g. "..") through GetFile(), GetDirectory(), or RemoveEntry() on a content-URI-backed handle to reference entries outside the directory the user granted, on Android (the branches are IsContentUri()-gated and the added test is #if BUILDFLAG(IS_ANDROID)). The precondition is possession of a content-URI-backed File System Access directory handle for which the origin holds the relevant read/write permission. The advisory classifies this as a LogicError / “Incorrect reference resolution in FileSystem”; it states no exploited-in-the-wild status, and any escape beyond the granted directory scope or downstream escalation is (inferred) rather than demonstrated by the patch.

Changed Functions

FunctionChangeNotes
TEST_F
content/browser/file_system_access/file_system_access_directory_handle_impl_unittest.cc
modified
for
content/browser/file_system_access/file_system_access_directory_handle_impl_unittest.cc
modified
if
content/browser/file_system_access/file_system_access_directory_handle_impl_unittest.cc
modified
FileSystemAccessDirectoryHandleImplRemoveTest
content/browser/file_system_access/file_system_access_directory_handle_impl_unittest.cc
modified

Files Changed

  • content/browser/file_system_access/file_system_access_directory_handle_impl.cc
  • content/browser/file_system_access/file_system_access_handle_base.h
  • content/browser/file_system_access/file_system_access_directory_handle_impl.h
  • content/browser/file_system_access/file_system_access_directory_handle_impl_unittest.cc

Audit Directions

  • Early-return branches that skip shared validation
    Audit any handler with a platform- or type-gated fast path (e.g. IsContentUri(), other BUILDFLAG branches) that returns before reaching the common helper where input validation lives, since the check can be silently bypassed.
  • Single-point validation reachable by multiple callers
    Where a safety predicate like IsSafePathComponent() is enforced in only one downstream helper (GetChildURL()), verify every caller path actually reaches it rather than short-circuiting first.
  • Attacker-controlled name/reference components in File System Access
    Review other basename/path-component-taking Mojo entry points in content/browser/file_system_access/ for traversal sequences ("", "..", "a/b", "a\\b") on content-URI and other non-standard backends.
From 9ab0e2155012b77f284a6c1f88df50fbcfd8eaa4 Mon Sep 17 00:00:00 2001
From: Eriko Kurimoto <elkurin@chromium.org>
Date: Mon, 03 Aug 2026 20:55:18 -0700
Subject: [PATCH] Prevent path traversal via content-URIs in FileSystemAccessDirectoryHandleImpl

GetFile(), GetDirectory(), and RemoveEntry() in
FileSystemAccessDirectoryHandleImpl did not validate that the child
basename was a safe path component when the handle was backed by a
content-URI, because they returned early before reaching GetChildURL() (which performs the check).

This allowed path traversal sequences (like "..") to bypass safety checks
on Android.

This CL adds IsSafePathComponent() checks at the entry points of these
methods before the content-URI early-return blocks.

Bug: 497111188
Change-Id: I1a8cb659953ec64c0dbeeed7e937777975d1c948
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8172946
Auto-Submit: Eriko Kurimoto <elkurin@chromium.org>
Commit-Queue: Mingyu Lei <leimy@chromium.org>
Reviewed-by: Mingyu Lei <leimy@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1673079}
---
diff --git a/content/browser/file_system_access/file_system_access_directory_handle_impl.cc b/content/browser/file_system_access/file_system_access_directory_handle_impl.cc
index 2de16b33..74be365e 100644
--- a/content/browser/file_system_access/file_system_access_directory_handle_impl.cc
+++ b/content/browser/file_system_access/file_system_access_directory_handle_impl.cc
@@ -140,6 +140,13 @@
   // and create the document. DidGetFile() will then update the child path
   // before creating the returned handle.
   if (url().virtual_path().IsContentUri()) {
+    if (!IsSafePathComponent(basename)) {
+      std::move(callback).Run(
+          file_system_access_error::FromStatus(
+              FileSystemAccessStatus::kInvalidArgument, "Name is not allowed."),
+          mojo::NullRemote());
+      return;
+    }
     std::string mime_type;
     if (!net::GetWellKnownMimeTypeFromFile(base::FilePath(basename),
                                            &mime_type)) {
@@ -281,6 +288,13 @@
   // and create the document. DidGetDirectory() will then update the child path
   // before creating the returned handle.
   if (url().virtual_path().IsContentUri()) {
+    if (!IsSafePathComponent(basename)) {
+      std::move(callback).Run(
+          file_system_access_error::FromStatus(
+              FileSystemAccessStatus::kInvalidArgument, "Name is not allowed."),
+          mojo::NullRemote());
+      return;
+    }
     base::ThreadPool::PostTaskAndReplyWithResult(
         FROM_HERE, {base::MayBlock(), base::TaskPriority::USER_VISIBLE},
         base::BindOnce(&base::ContentUriGetChildDocumentOrQuery,
@@ -458,6 +472,11 @@
 #if BUILDFLAG(IS_ANDROID)
   // Lookup content-URI by display-name.
   if (url().virtual_path().IsContentUri()) {
+    if (!IsSafePathComponent(basename)) {
+      std::move(callback).Run(file_system_access_error::FromStatus(
+          FileSystemAccessStatus::kInvalidArgument, "Name is not allowed."));
+      return;
+    }
     base::ThreadPool::PostTaskAndReplyWithResult(
         FROM_HERE, {base::MayBlock(), base::TaskPriority::USER_VISIBLE},
         base::BindOnce(&base::ContentUriGetChildDocumentOrQuery,
@@ -913,18 +932,23 @@
                                               more_batches_are_expected);
 }
 
+bool FileSystemAccessDirectoryHandleImpl::IsSafePathComponent(
+    const std::string& basename) const {
+  return manager()->IsSafePathComponent(url().type(), basename);
+}
+
 blink::mojom::FileSystemAccessErrorPtr
 FileSystemAccessDirectoryHandleImpl::GetChildURL(
     const std::string& basename,
     storage::FileSystemURL* result) {
   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
 
-  const storage::FileSystemURL& parent = url();
-  if (!manager()->IsSafePathComponent(parent.type(), basename)) {
+  if (!IsSafePathComponent(basename)) {
     return file_system_access_error::FromStatus(
         FileSystemAccessStatus::kInvalidArgument, "Name is not allowed.");
   }
 
+  const storage::FileSystemURL& parent = url();
 #if BUILDFLAG(IS_ANDROID)
   base::FilePath child_path =
       parent.virtual_path().IsContentUri()
diff --git a/content/browser/file_system_access/file_system_access_directory_handle_impl.h b/content/browser/file_system_access/file_system_access_directory_handle_impl.h
index 39f71540..f0770dc 100644
--- a/content/browser/file_system_access/file_system_access_directory_handle_impl.h
+++ b/content/browser/file_system_access/file_system_access_directory_handle_impl.h
@@ -180,6 +180,8 @@
           std::vector<blink::mojom::FileSystemAccessEntryPtr>)> final_callback,
       std::vector<blink::mojom::FileSystemAccessEntryPtr> entries);
 
+  bool IsSafePathComponent(const std::string& basename) const;
+
   storage::FileSystemURL CreateChildURL(const base::FilePath& child_path);
 
   // Helper to create a blink::mojom::FileSystemAccessEntry struct.
diff --git a/content/browser/file_system_access/file_system_access_handle_base.h b/content/browser/file_system_access/file_system_access_handle_base.h
index b43a70c0..e9bebb2 100644
--- a/content/browser/file_system_access/file_system_access_handle_base.h
+++ b/content/browser/file_system_access/file_system_access_handle_base.h
@@ -60,8 +60,8 @@
   const storage::FileSystemURL& url() const { return url_; }
   const SharedHandleState& handle_state() const { return handle_state_; }
   const BindingContext& context() const { return context_; }
-  FileSystemAccessManagerImpl* manager() { return manager_; }
-  storage::FileSystemContext* file_system_context() {
+  FileSystemAccessManagerImpl* manager() const { return manager_; }
+  storage::FileSystemContext* file_system_context() const {
     return manager()->context();
   }
 
diff --git a/content/browser/file_system_access/file_system_access_directory_handle_impl_unittest.cc b/content/browser/file_system_access/file_system_access_directory_handle_impl_unittest.cc
index 1874eb8c..7556b8d7 100644
--- a/content/browser/file_system_access/file_system_access_directory_handle_impl_unittest.cc
+++ b/content/browser/file_system_access/file_system_access_directory_handle_impl_unittest.cc
@@ -42,6 +42,10 @@
 #include "third_party/blink/public/common/storage_key/storage_key.h"
 #include "url/gurl.h"
 
+#if BUILDFLAG(IS_ANDROID)
+#include "base/test/android/content_uri_test_utils.h"
+#endif
+
 namespace content {
 namespace {
 using storage::FileSystemURL;
@@ -471,6 +475,95 @@
   EXPECT_TRUE(entries.empty());
 }
 
+TEST_F(FileSystemAccessDirectoryHandleImplTest, InvalidPathComponent) {
+  constexpr const char* kInvalidNames[] = {"", "..", "../a", "a/b", "a\\b"};
+  for (const char* name : kInvalidNames) {
+    SCOPED_TRACE(name);
+    {
+      base::test::TestFuture<
+          blink::mojom::FileSystemAccessErrorPtr,
+          mojo::PendingRemote<blink::mojom::FileSystemAccessFileHandle>>
+          future;
+      handle_->GetFile(name, /*create=*/false, future.GetCallback());
+      EXPECT_EQ(future.Get<0>()->status,
+                blink::mojom::FileSystemAccessStatus::kInvalidArgument);
+      EXPECT_FALSE(future.Get<1>().is_valid());
+    }
+    {
+      base::test::TestFuture<
+          blink::mojom::FileSystemAccessErrorPtr,
+          mojo::PendingRemote<blink::mojom::FileSystemAccessDirectoryHandle>>
+          future;
+      handle_->GetDirectory(name, /*create=*/false, future.GetCallback());
+      EXPECT_EQ(future.Get<0>()->status,
+                blink::mojom::FileSystemAccessStatus::kInvalidArgument);
+      EXPECT_FALSE(future.Get<1>().is_valid());
+    }
+    {
+      base::test::TestFuture<blink::mojom::FileSystemAccessErrorPtr> future;
+      handle_->RemoveEntry(name, /*recurse=*/false, future.GetCallback());
+      EXPECT_EQ(future.Get()->status,
+                blink::mojom::FileSystemAccessStatus::kInvalidArgument);
+    }
+  }
+}
+
+#if BUILDFLAG(IS_ANDROID)
+// Verifies that `GetFile`, `GetDirectory` and `RemoveEntry` reject names that
+// are not valid path components when the directory handle is backed by a
+// content URI.
+TEST_F(FileSystemAccessDirectoryHandleImplTest, ContentUri_InvalidName) {
+  std::optional<base::FilePath> content_uri =
+      base::test::android::GetInMemoryContentTreeUriFromCacheDirDirectory(
+          dir_.GetPath());
+  EXPECT_TRUE(content_uri.has_value());
+  if (!content_uri.has_value()) {
+    return;
+  }
+  EXPECT_TRUE(content_uri->IsContentUri());
+  if (!content_uri->IsContentUri()) {
+    return;
+  }
+  auto handle =
+      GetHandleWithPermissions(*content_uri, /*read=*/true, /*write=*/true);
+  EXPECT_TRUE(handle);
+  if (!handle) {
+    return;
+  }
+
+  constexpr const char* kInvalidNames[] = {"", "..", "../a", "a/b", "a\\b"};
+  for (const char* name : kInvalidNames) {
+    SCOPED_TRACE(name);
+    {
+      base::test::TestFuture<
+          blink::mojom::FileSystemAccessErrorPtr,
+          mojo::PendingRemote<blink::mojom::FileSystemAccessFileHandle>>
+          future;
+      handle->GetFile(name, /*create=*/false, future.GetCallback());
+      EXPECT_EQ(future.Get<0>()->status,
+                blink::mojom::FileSystemAccessStatus::kInvalidArgument);
+      EXPECT_FALSE(future.Get<1>().is_valid());
+    }
+    {
+      base::test::TestFuture<
+          blink::mojom::FileSystemAccessErrorPtr,
+          mojo::PendingRemote<blink::mojom::FileSystemAccessDirectoryHandle>>
+          future;
+      handle->GetDirectory(name, /*create=*/false, future.GetCallback());
+      EXPECT_EQ(future.Get<0>()->status,
+                blink::mojom::FileSystemAccessStatus::kInvalidArgument);
+      EXPECT_FALSE(future.Get<1>().is_valid());
+    }
+    {
+      base::test::TestFuture<blink::mojom::FileSystemAccessErrorPtr> future;
+      handle->RemoveEntry(name, /*recurse=*/false, future.GetCallback());
+      EXPECT_EQ(future.Get()->status,
+                blink::mojom::FileSystemAccessStatus::kInvalidArgument);
+    }
+  }
+}
+#endif  // BUILDFLAG(IS_ANDROID)
+
 // Tests for `FileSystemAccessDirectoryHandleImpl::Remove()`.
 class FileSystemAccessDirectoryHandleImplRemoveTest
     : public FileSystemAccessDirectoryHandleImplPermissionTestBase,
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/content/browser/file_system_access/file_system_access_directory_handle_impl_unittest.cc b/content/browser/file_system_access/file_system_access_directory_handle_impl_unittest.cc
index 1874eb8c..7556b8d7 100644
--- a/content/browser/file_system_access/file_system_access_directory_handle_impl_unittest.cc
+++ b/content/browser/file_system_access/file_system_access_directory_handle_impl_unittest.cc
@@ -42,6 +42,10 @@
 #include "third_party/blink/public/common/storage_key/storage_key.h"
 #include "url/gurl.h"
 
+#if BUILDFLAG(IS_ANDROID)
+#include "base/test/android/content_uri_test_utils.h"
+#endif
+
 namespace content {
 namespace {
 using storage::FileSystemURL;
@@ -471,6 +475,95 @@
   EXPECT_TRUE(entries.empty());
 }
 
+TEST_F(FileSystemAccessDirectoryHandleImplTest, InvalidPathComponent) {
+  constexpr const char* kInvalidNames[] = {"", "..", "../a", "a/b", "a\\b"};
+  for (const char* name : kInvalidNames) {
+    SCOPED_TRACE(name);
+    {
+      base::test::TestFuture<
+          blink::mojom::FileSystemAccessErrorPtr,
+          mojo::PendingRemote<blink::mojom::FileSystemAccessFileHandle>>
+          future;
+      handle_->GetFile(name, /*create=*/false, future.GetCallback());
+      EXPECT_EQ(future.Get<0>()->status,
+                blink::mojom::FileSystemAccessStatus::kInvalidArgument);
+      EXPECT_FALSE(future.Get<1>().is_valid());
+    }
+    {
+      base::test::TestFuture<
+          blink::mojom::FileSystemAccessErrorPtr,
+          mojo::PendingRemote<blink::mojom::FileSystemAccessDirectoryHandle>>
+          future;
+      handle_->GetDirectory(name, /*create=*/false, future.GetCallback());
+      EXPECT_EQ(future.Get<0>()->status,
+                blink::mojom::FileSystemAccessStatus::kInvalidArgument);
+      EXPECT_FALSE(future.Get<1>().is_valid());
+    }
+    {
+      base::test::TestFuture<blink::mojom::FileSystemAccessErrorPtr> future;
+      handle_->RemoveEntry(name, /*recurse=*/false, future.GetCallback());
+      EXPECT_EQ(future.Get()->status,
+                blink::mojom::FileSystemAccessStatus::kInvalidArgument);
+    }
+  }
+}
+
+#if BUILDFLAG(IS_ANDROID)
+// Verifies that `GetFile`, `GetDirectory` and `RemoveEntry` reject names that
+// are not valid path components when the directory handle is backed by a
+// content URI.
+TEST_F(FileSystemAccessDirectoryHandleImplTest, ContentUri_InvalidName) {
+  std::optional<base::FilePath> content_uri =
+      base::test::android::GetInMemoryContentTreeUriFromCacheDirDirectory(
+          dir_.GetPath());
+  EXPECT_TRUE(content_uri.has_value());
+  if (!content_uri.has_value()) {
+    return;
+  }
+  EXPECT_TRUE(content_uri->IsContentUri());
+  if (!content_uri->IsContentUri()) {
+    return;
+  }
+  auto handle =
+      GetHandleWithPermissions(*content_uri, /*read=*/true, /*write=*/true);
+  EXPECT_TRUE(handle);
+  if (!handle) {
+    return;
+  }
+
+  constexpr const char* kInvalidNames[] = {"", "..", "../a", "a/b", "a\\b"};
+  for (const char* name : kInvalidNames) {
+    SCOPED_TRACE(name);
+    {
+      base::test::TestFuture<
+          blink::mojom::FileSystemAccessErrorPtr,
+          mojo::PendingRemote<blink::mojom::FileSystemAccessFileHandle>>
+          future;
+      handle->GetFile(name, /*create=*/false, future.GetCallback());
+      EXPECT_EQ(future.Get<0>()->status,
+                blink::mojom::FileSystemAccessStatus::kInvalidArgument);
+      EXPECT_FALSE(future.Get<1>().is_valid());
+    }
+    {
+      base::test::TestFuture<
+          blink::mojom::FileSystemAccessErrorPtr,
+          mojo::PendingRemote<blink::mojom::FileSystemAccessDirectoryHandle>>
+          future;
+      handle->GetDirectory(name, /*create=*/false, future.GetCallback());
+      EXPECT_EQ(future.Get<0>()->status,
+                blink::mojom::FileSystemAccessStatus::kInvalidArgument);
+      EXPECT_FALSE(future.Get<1>().is_valid());
+    }
+    {
+      base::test::TestFuture<blink::mojom::FileSystemAccessErrorPtr> future;
+      handle->RemoveEntry(name, /*recurse=*/false, future.GetCallback());
+      EXPECT_EQ(future.Get()->status,
+                blink::mojom::FileSystemAccessStatus::kInvalidArgument);
+    }
+  }
+}
+#endif  // BUILDFLAG(IS_ANDROID)
+
 // Tests for `FileSystemAccessDirectoryHandleImpl::Remove()`.
 class FileSystemAccessDirectoryHandleImplRemoveTest
     : public FileSystemAccessDirectoryHandleImplPermissionTestBase,
Loading diff…

Original Bug Report

reported by rj...@google.com

Potential path validation bypass in FileSystemAccess API for Android Content URIs

Project Fortify, an experimental security project, has identified the following potential security issue.

Overview: The File System Access API on Android fails to validate the child entry name (basename) when interacting with Content URI-backed directories. This potential flaw allows a malicious renderer to pass path traversal sequences directly to external Android DocumentsProviders. If the provider lacks its own sanitization, this bypasses directory scoping guarantees and escapes the sandbox.

Affected files:

  • content/browser/file_system_access/file_system_access_directory_handle_impl.cc

Estimated timestamp from git blame: 2025-03-17

Summary There is a potential path validation bypass in the File System Access API implementation on Android. When a renderer requests a file, directory, or deletion using a basename, the browser-side implementation is supposed to validate this component using manager()->IsSafePathComponent(...) to ensure it does not contain dangerous characters or traversal sequences (e.g., .., /, \). However, on Android, this validation is skipped for Content URIs, and the unvalidated string is passed directly via JNI to Android’s DocumentsProvider IPC.

Technical Analysis In content/browser/file_system_access/file_system_access_directory_handle_impl.cc, the methods GetFile, GetDirectory, and RemoveEntry check if the directory handle’s path is a Content URI.

// content/browser/file_system_access/file_system_access_directory_handle_impl.cc
void FileSystemAccessDirectoryHandleImpl::GetFile(...) {
#if BUILDFLAG(IS_ANDROID)
  if (url().virtual_path().IsContentUri()) {
    // ...
    base::ThreadPool::PostTaskAndReplyWithResult(
        FROM_HERE, {base::MayBlock(), base::TaskPriority::USER_VISIBLE},
        base::BindOnce(&base::ContentUriGetChildDocumentOrQuery,
                       url().virtual_path(), basename, mime_type,
                       /*is_directory=*/false, create),
        // ...
    return; // <-- BYPASS: Early return skips validation
  }
#endif

  storage::FileSystemURL child_url;
  blink::mojom::FileSystemAccessErrorPtr get_child_url_result =
      GetChildURL(basename, &child_url); // <-- VALIDATION OCCURS HERE
  // ...
}

When url().virtual_path().IsContentUri() is true, the code immediately posts a task to the ThreadPool and returns. It never reaches the subsequent call to GetChildURL(basename, &child_url), which is where the critical validation occurs via manager()->IsSafePathComponent(...).

By bypassing this check, a compromised renderer (or even standard JavaScript) can pass arbitrary strings as the basename. This string flows through base::ContentUriGetChildDocumentOrQuery into Java (ContentUriUtils.java), where it is eventually passed as the displayName parameter to DocumentsContract.createDocument(resolver, parentUri, mimeType, displayName).

While some system-provided DocumentsProviders may sanitize inputs internally, relying on external third-party providers (such as cloud storage apps or custom file managers) to handle path traversal sequences safely is a “confused deputy” risk that breaks the API’s fundamental directory scoping guarantees.

Suggested Steps to Reproduce (Note: These are suggested/potential steps as our tooling agent does not currently have the ability to run code and verify the exploit end-to-end.)

  1. An attacker hosts a malicious webpage that invokes await self.showDirectoryPicker().
  2. The user is prompted and grants access to an Android directory backed by a Content URI (e.g., an external SD card or a third-party DocumentProvider).
  3. The webpage receives the FileSystemDirectoryHandle.
  4. The webpage executes await dirHandle.getFileHandle('../malicious_file', {create: true}).
  5. The ../malicious_file payload is sent unvalidated to the browser, bypassing IsSafePathComponent, and is passed directly to the external DocumentsProvider.
  6. If the provider is vulnerable, it creates a file outside the user-approved directory scope.

Suggested Fix The manager()->IsSafePathComponent(...) check should be decoupled from GetChildURL() or explicitly called at the very beginning of GetFile, GetDirectory, and RemoveEntry before the #if BUILDFLAG(IS_ANDROID) Content URI early-return blocks.

Evaluated with Chrome root at commit: 876d480da1f794d87813cfa2e6ff4fcf9771e939


Results from 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