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 WebAppInstalls
DescriptionInsufficient validation of untrusted input in WebAppInstalls
ComponentWebAppInstalls
Bug ClassLogic Error
Tracker497977983
Fix commitd13dce3ac390 (chromium/src) +80/-1
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-30

Changed Functions

FunctionChangeNotes
for
chrome/browser/android/webapps/twa_launch_queue_delegate.cc
modified
if
components/webapps/browser/launch_queue/launch_queue.cc
modified
LaunchQueueTest
components/webapps/browser/launch_queue/launch_queue_unittest.cc
modified
TEST_F
components/webapps/browser/launch_queue/launch_queue_unittest.cc
modified

Files Changed

  • chrome/browser/android/webapps/twa_launch_queue_delegate.cc
  • components/webapps/browser/launch_queue/launch_queue.cc
  • components/webapps/browser/launch_queue/launch_queue_unittest.cc
From d13dce3ac390ec55d256e02edf6d4752c17eb4e5 Mon Sep 17 00:00:00 2001
From: Nate Chapin <japhet@chromium.org>
Date: Thu, 28 May 2026 12:38:44 -0700
Subject: [PATCH] FileSystemAccess: Block sensitive paths in TWA launch

A malicious Android application could bypass the FileSystemAccess API
blocklist to obtain read and write access to arbitrary files within
Chrome's private data directory via TWA launch intents.

This CL implements path validation in TwaLaunchQueueDelegate to block
sensitive paths (parent references, Chrome's own Content URIs, app data
and cache directories, system directories).
It also modifies LaunchQueue to gracefully clear invalid paths instead
of crashing or bypassing the check.

TAG=agy
CONV=614f9db2-61b7-44b4-830b-5ebe9a33ae04

Bug: 497977983
Change-Id: I18debb3231454f5a7885952f58f66f42862499c9
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7865764
Reviewed-by: Daniel Murphy <dmurph@chromium.org>
Reviewed-by: Glenn Hartmann <hartmanng@chromium.org>
Commit-Queue: Nate Chapin <japhet@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1637880}
---

diff --git a/chrome/browser/android/webapps/twa_launch_queue_delegate.cc b/chrome/browser/android/webapps/twa_launch_queue_delegate.cc
index f4f426b..d14eb41e 100644
--- a/chrome/browser/android/webapps/twa_launch_queue_delegate.cc
+++ b/chrome/browser/android/webapps/twa_launch_queue_delegate.cc
@@ -4,14 +4,63 @@
 
 #include "chrome/browser/android/webapps/twa_launch_queue_delegate.h"
 
+#include "base/android/apk_info.h"
 #include "base/files/file_path.h"
+#include "base/strings/strcat.h"
+#include "base/strings/string_util.h"
 #include "components/webapps/browser/launch_queue/launch_params.h"
 #include "content/public/browser/file_system_access_permission_context.h"
 
 namespace webapps {
 
+namespace {
+
+bool IsSensitivePath(const base::FilePath& path) {
+  if (path.empty()) {
+    return true;
+  }
+
+  // Block all absolute paths. Legitimate file launching on Android should
+  // use Content URIs.
+  if (path.IsAbsolute()) {
+    return true;
+  }
+
+  if (path.ReferencesParent()) {
+    return true;
+  }
+
+  // Check Content URIs
+  if (path.IsContentUri()) {
+    // Block Chrome's own Content URIs
+    std::string package_name = base::android::apk_info::package_name();
+    std::string chrome_content_prefix =
+        base::StrCat({"content://", package_name, "."});
+    return base::StartsWith(path.value(), chrome_content_prefix,
+                            base::CompareCase::INSENSITIVE_ASCII);
+  }
+
+  // Block file:// URIs as they can be used to specify absolute paths
+  if (base::StartsWith(path.value(), "file://",
+                       base::CompareCase::INSENSITIVE_ASCII)) {
+    return true;
+  }
+
+  return false;
+}
+
+}  // namespace
+
 bool TwaLaunchQueueDelegate::IsValidLaunchParams(
     const webapps::LaunchParams& launch_params) const {
+  if (!launch_params.dir.empty() && IsSensitivePath(launch_params.dir)) {
+    return false;
+  }
+  for (const auto& path : launch_params.paths) {
+    if (IsSensitivePath(path)) {
+      return false;
+    }
+  }
   return true;
 }
 
diff --git a/components/webapps/browser/launch_queue/launch_queue.cc b/components/webapps/browser/launch_queue/launch_queue.cc
index b5516d8..e8e8399 100644
--- a/components/webapps/browser/launch_queue/launch_queue.cc
+++ b/components/webapps/browser/launch_queue/launch_queue.cc
@@ -87,7 +87,10 @@
   DCHECK(delegate_->IsInScope(launch_params, launch_params.target_url))
       << launch_params.target_url.spec();
 
-  DCHECK(delegate_->IsValidLaunchParams(launch_params));
+  if (!delegate_->IsValidLaunchParams(launch_params)) {
+    launch_params.paths.clear();
+    launch_params.dir.clear();
+  }
 
   if (launch_params.started_new_navigation) {
     Reset();
diff --git a/components/webapps/browser/launch_queue/launch_queue_unittest.cc b/components/webapps/browser/launch_queue/launch_queue_unittest.cc
index a0cbb77..68dcdd9e 100644
--- a/components/webapps/browser/launch_queue/launch_queue_unittest.cc
+++ b/components/webapps/browser/launch_queue/launch_queue_unittest.cc
@@ -62,20 +62,26 @@
       std::vector<blink::mojom::FileSystemAccessEntryPtr> files) override {
     launched_url_ = launch_url;
     enqueue_called_ = true;
+    files_ = std::move(files);
   }
 
   bool enqueue_called() const { return enqueue_called_; }
   const GURL& launched_url() const { return launched_url_; }
+  const std::vector<blink::mojom::FileSystemAccessEntryPtr>& files() const {
+    return files_;
+  }
 
   void Reset() {
     enqueue_called_ = false;
     launched_url_ = GURL();
+    files_.clear();
   }
 
  private:
   mojo::AssociatedReceiver<blink::mojom::WebLaunchService> receiver_{this};
   bool enqueue_called_ = false;
   GURL launched_url_;
+  std::vector<blink::mojom::FileSystemAccessEntryPtr> files_;
 };
 
 class LaunchQueueTest : public content::RenderViewHostTestHarness {
@@ -203,4 +209,25 @@
       launch_queue_->GetPendingLaunchAppId());  // Queue should be reset
 }
 
+TEST_F(LaunchQueueTest, EnqueueInvalidParams) {
+  GURL launch_url("https://example.com/launch");
+  LaunchParams params = CreateLaunchParams(launch_url);
+  params.paths.push_back(
+      base::FilePath(FILE_PATH_LITERAL("sensitive_file.txt")));
+
+  EXPECT_CALL(*delegate_, IsValidLaunchParams(testing::_))
+      .WillOnce(testing::Return(false));
+
+  launch_queue_->Enqueue(std::move(params));
+  EXPECT_TRUE(launch_queue_->GetPendingLaunchAppId());
+
+  content::NavigationSimulator::NavigateAndCommitFromBrowser(web_contents(),
+                                                             launch_url);
+
+  launch_queue_->FlushForTesting();
+
+  EXPECT_TRUE(fake_launch_service_.enqueue_called());
+  EXPECT_TRUE(fake_launch_service_.files().empty());
+}
+
 }  // namespace webapps
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/components/webapps/browser/launch_queue/launch_queue_unittest.cc b/components/webapps/browser/launch_queue/launch_queue_unittest.cc
index a0cbb77..68dcdd9e 100644
--- a/components/webapps/browser/launch_queue/launch_queue_unittest.cc
+++ b/components/webapps/browser/launch_queue/launch_queue_unittest.cc
@@ -62,20 +62,26 @@
       std::vector<blink::mojom::FileSystemAccessEntryPtr> files) override {
     launched_url_ = launch_url;
     enqueue_called_ = true;
+    files_ = std::move(files);
   }
 
   bool enqueue_called() const { return enqueue_called_; }
   const GURL& launched_url() const { return launched_url_; }
+  const std::vector<blink::mojom::FileSystemAccessEntryPtr>& files() const {
+    return files_;
+  }
 
   void Reset() {
     enqueue_called_ = false;
     launched_url_ = GURL();
+    files_.clear();
   }
 
  private:
   mojo::AssociatedReceiver<blink::mojom::WebLaunchService> receiver_{this};
   bool enqueue_called_ = false;
   GURL launched_url_;
+  std::vector<blink::mojom::FileSystemAccessEntryPtr> files_;
 };
 
 class LaunchQueueTest : public content::RenderViewHostTestHarness {
@@ -203,4 +209,25 @@
       launch_queue_->GetPendingLaunchAppId());  // Queue should be reset
 }
 
+TEST_F(LaunchQueueTest, EnqueueInvalidParams) {
+  GURL launch_url("https://example.com/launch");
+  LaunchParams params = CreateLaunchParams(launch_url);
+  params.paths.push_back(
+      base::FilePath(FILE_PATH_LITERAL("sensitive_file.txt")));
+
+  EXPECT_CALL(*delegate_, IsValidLaunchParams(testing::_))
+      .WillOnce(testing::Return(false));
+
+  launch_queue_->Enqueue(std::move(params));
+  EXPECT_TRUE(launch_queue_->GetPendingLaunchAppId());
+
+  content::NavigationSimulator::NavigateAndCommitFromBrowser(web_contents(),
+                                                             launch_url);
+
+  launch_queue_->FlushForTesting();
+
+  EXPECT_TRUE(fake_launch_service_.enqueue_called());
+  EXPECT_TRUE(fake_launch_service_.files().empty());
+}
+
 }  // namespace webapps
Loading diff…

Original Bug Report

reported by vm...@google.com

Potential FileSystemAccess Blocklist Bypass via TWA LaunchQueue Intent on Android

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

Overview: A malicious Android application can potentially bypass the FileSystemAccess API blocklist to obtain read and write access to arbitrary files within Chrome’s private data directory (e.g., Cookies). This occurs because Trusted Web Activity (TWA) intents with EXTRA_FILE_HANDLING_DATA pass unsanitized file paths directly to the LaunchQueue, which creates file handles with auto-granted permissions without verifying if the paths are sensitive.

Affected files:

  • chrome/browser/android/webapps/twa_launch_queue_delegate.cc
  • components/webapps/browser/launch_queue/launch_queue.cc
  • chrome/browser/android/webapps/web_app_launch_handler.cc
  • content/browser/file_system_access/file_system_access_manager_impl.cc
  • chrome/browser/file_system_access/chrome_file_system_access_permission_context.cc

Estimated timestamp from git blame: 2026-02-18

Overview

The Trusted Web Activity (TWA) launch flow on Android contains a potential vulnerability that allows a malicious application to grant its corresponding web origin full read/write access to Chrome’s internal profile directory and other sensitive system files.

When a TWA is launched via an intent, it can include EXTRA_FILE_HANDLING_DATA containing a list of file URIs. These URIs are passed from Java to the browser process via WebAppLaunchHandler::NotifyLaunchQueue without any path validation or sanitization.

Vulnerability Details

  1. Unsanitized Input: In chrome/android/java/src/org/chromium/chrome/browser/customtabs/content/WebAppLaunchHandler.java, the EXTRA_FILE_HANDLING_DATA bundle is parsed, and its URIs are converted to strings. These strings are passed to the native JNI method notifyLaunchQueue without any path validation or resolution.
  2. Implicit Conversion: In chrome/browser/android/webapps/web_app_launch_handler.cc, these strings are implicitly converted to base::FilePath objects and added to a webapps::LaunchParams object.
  3. Validation Bypass: The webapps::LaunchQueue attempts to validate these parameters using its delegate (TwaLaunchQueueDelegate). However, TwaLaunchQueueDelegate::IsValidLaunchParams is a stub that always returns true, performing no checks on the provided paths.
  4. Blocklist Evasion: LaunchQueue::SendLaunchParams processes these paths by calling FileSystemAccessManagerImpl::CreateFileEntryFromPath with the UserAction::kSave parameter. Crucially, CreateFileEntryFromPath does not invoke ConfirmSensitiveEntryAccess, completely bypassing the FileSystem Access blocklist that normally protects sensitive directories like /data/data/com.android.chrome/.
  5. Auto-Granted Permissions: CreateFileEntryFromPath requests permissions from ChromeFileSystemAccessPermissionContext. Because the action is UserAction::kSave (which normally implies a user explicitly chose a file via a picker), the context automatically grants both read and write permissions without prompting the user.
  6. Delivery to Origin: The resulting FileSystemFileHandle is delivered to the attacker’s origin via the window.launchQueue.setConsumer JavaScript API.

Potential Attacker Steps

Note: These are suggested/potential steps; our tooling agent does not currently have the ability to run code to verify this with a live Proof of Concept.

  1. An attacker creates a malicious Android app and a corresponding website, establishing a TWA relationship via Digital Asset Links.
  2. The malicious app constructs an intent to launch its TWA.
  3. The app attaches an EXTRA_FILE_HANDLING_DATA bundle to the intent, containing absolute path strings pointing to sensitive files (e.g., /data/data/com.android.chrome/app_chrome/Default/Cookies).
  4. The app fires the intent, launching Chrome.
  5. The attacker’s TWA website uses the window.launchQueue API to receive the FileSystemFileHandle.
  6. The website calls handle.getFile() to read the sensitive file’s contents, stealing user data or session tokens.

Suggested Fix

To remediate this issue, the browser must validate the paths provided in the launch parameters against the FileSystemAccess blocklist before creating file handles.

  1. Update CreateFileEntryFromPath: Modify FileSystemAccessManagerImpl::CreateFileEntryFromPath (and CreateDirectoryEntryFromPath) to invoke ConfirmSensitiveEntryAccess and handle the asynchronous result before granting access, similar to how DidChooseEntries operates.
  2. Enhance TwaLaunchQueueDelegate: Implement proper validation in TwaLaunchQueueDelegate::IsValidLaunchParams to ensure that paths provided via Android intents are safe and do not point to sensitive directories or files.
  3. Sanitize URIs in Java: Consider adding validation or resolution logic in WebAppLaunchHandler.java to ensure that only expected file:// or content:// URIs are processed, preventing the direct use of absolute paths.

Evaluated with Chrome root at commit: a9cbf6e8b275fe4147435aa905f3b7f5a656f5f0


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