CVE-2026-13872
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
forchrome/browser/android/webapps/twa_launch_queue_delegate.cc |
modified | |
ifcomponents/webapps/browser/launch_queue/launch_queue.cc |
modified | |
LaunchQueueTestcomponents/webapps/browser/launch_queue/launch_queue_unittest.cc |
modified | |
TEST_Fcomponents/webapps/browser/launch_queue/launch_queue_unittest.cc |
modified |
Files Changed
chrome/browser/android/webapps/twa_launch_queue_delegate.cccomponents/webapps/browser/launch_queue/launch_queue.cccomponents/webapps/browser/launch_queue/launch_queue_unittest.cc
Patch
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
Regression Test / PoC
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
Original Bug Report
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.cccomponents/webapps/browser/launch_queue/launch_queue.ccchrome/browser/android/webapps/web_app_launch_handler.cccontent/browser/file_system_access/file_system_access_manager_impl.ccchrome/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
- Unsanitized Input: In
chrome/android/java/src/org/chromium/chrome/browser/customtabs/content/WebAppLaunchHandler.java, theEXTRA_FILE_HANDLING_DATAbundle is parsed, and its URIs are converted to strings. These strings are passed to the native JNI methodnotifyLaunchQueuewithout any path validation or resolution. - Implicit Conversion: In
chrome/browser/android/webapps/web_app_launch_handler.cc, these strings are implicitly converted tobase::FilePathobjects and added to awebapps::LaunchParamsobject. - Validation Bypass: The
webapps::LaunchQueueattempts to validate these parameters using its delegate (TwaLaunchQueueDelegate). However,TwaLaunchQueueDelegate::IsValidLaunchParamsis a stub that always returnstrue, performing no checks on the provided paths. - Blocklist Evasion:
LaunchQueue::SendLaunchParamsprocesses these paths by callingFileSystemAccessManagerImpl::CreateFileEntryFromPathwith theUserAction::kSaveparameter. Crucially,CreateFileEntryFromPathdoes not invokeConfirmSensitiveEntryAccess, completely bypassing the FileSystem Access blocklist that normally protects sensitive directories like/data/data/com.android.chrome/. - Auto-Granted Permissions:
CreateFileEntryFromPathrequests permissions fromChromeFileSystemAccessPermissionContext. Because the action isUserAction::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. - Delivery to Origin: The resulting
FileSystemFileHandleis delivered to the attacker’s origin via thewindow.launchQueue.setConsumerJavaScript 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.
- An attacker creates a malicious Android app and a corresponding website, establishing a TWA relationship via Digital Asset Links.
- The malicious app constructs an intent to launch its TWA.
- The app attaches an
EXTRA_FILE_HANDLING_DATAbundle to the intent, containing absolute path strings pointing to sensitive files (e.g.,/data/data/com.android.chrome/app_chrome/Default/Cookies). - The app fires the intent, launching Chrome.
- The attacker’s TWA website uses the
window.launchQueueAPI to receive theFileSystemFileHandle. - 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.
- Update
CreateFileEntryFromPath: ModifyFileSystemAccessManagerImpl::CreateFileEntryFromPath(andCreateDirectoryEntryFromPath) to invokeConfirmSensitiveEntryAccessand handle the asynchronous result before granting access, similar to howDidChooseEntriesoperates. - Enhance
TwaLaunchQueueDelegate: Implement proper validation inTwaLaunchQueueDelegate::IsValidLaunchParamsto ensure that paths provided via Android intents are safe and do not point to sensitive directories or files. - Sanitize URIs in Java: Consider adding validation or resolution logic in
WebAppLaunchHandler.javato ensure that only expectedfile://orcontent://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.