CVE-2026-17741
Overview
Files Changed
android_webview/java/src/org/chromium/android_webview/AwContentsClient.javaandroid_webview/javatests/src/org/chromium/android_webview/test/AwContentsClientShouldOverrideUrlLoadingTest.java
Patch
From cfbbdab6693deb0c048ec5e20891fd041a066266 Mon Sep 17 00:00:00 2001
From: Michael <mvanouwerkerk@chromium.org>
Date: Fri, 26 Jun 2026 07:50:15 -0700
Subject: [PATCH] Use intent flag allowlist.
Bug: 498877660
Change-Id: I86d05afed3b5d3cc4e928e21c815f0aae9f5f671
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8010508
Reviewed-by: Richard Coles <torne@chromium.org>
Commit-Queue: Richard Coles <torne@chromium.org>
Auto-Submit: Michael van Ouwerkerk <mvanouwerkerk@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1653154}
---
diff --git a/android_webview/java/src/org/chromium/android_webview/AwContentsClient.java b/android_webview/java/src/org/chromium/android_webview/AwContentsClient.java
index 5dd2766f..82557f14 100644
--- a/android_webview/java/src/org/chromium/android_webview/AwContentsClient.java
+++ b/android_webview/java/src/org/chromium/android_webview/AwContentsClient.java
@@ -89,6 +89,19 @@
private static final Pattern FILE_ANDROID_ASSET_PATTERN =
Pattern.compile("^file:///android_(asset|res)/.*");
+ // A mask of flags that are safe for untrusted content to use when starting an Activity.
+ // This list is not exhaustive and flags not listed here are not necessarily unsafe.
+ private static final int ALLOWED_INTENT_FLAGS =
+ Intent.FLAG_EXCLUDE_STOPPED_PACKAGES
+ | Intent.FLAG_ACTIVITY_CLEAR_TOP
+ | Intent.FLAG_ACTIVITY_SINGLE_TOP
+ | Intent.FLAG_ACTIVITY_MATCH_EXTERNAL
+ | Intent.FLAG_ACTIVITY_NEW_TASK
+ | Intent.FLAG_ACTIVITY_MULTIPLE_TASK
+ | Intent.FLAG_ACTIVITY_NEW_DOCUMENT
+ | Intent.FLAG_ACTIVITY_RETAIN_IN_RECENTS
+ | Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT;
+
public AwContentsClient() {
this(Looper.myLooper());
}
@@ -229,6 +242,7 @@
}
// Sanitize the Intent, ensuring web pages can not bypass browser
// security (only access to BROWSABLE activities).
+ intent.setFlags(intent.getFlags() & ALLOWED_INTENT_FLAGS);
intent.addCategory(Intent.CATEGORY_BROWSABLE);
intent.setComponent(null);
diff --git a/android_webview/javatests/src/org/chromium/android_webview/test/AwContentsClientShouldOverrideUrlLoadingTest.java b/android_webview/javatests/src/org/chromium/android_webview/test/AwContentsClientShouldOverrideUrlLoadingTest.java
index b3e9a937..938f58a 100644
--- a/android_webview/javatests/src/org/chromium/android_webview/test/AwContentsClientShouldOverrideUrlLoadingTest.java
+++ b/android_webview/javatests/src/org/chromium/android_webview/test/AwContentsClientShouldOverrideUrlLoadingTest.java
@@ -8,6 +8,7 @@
import static org.chromium.android_webview.test.AwActivityTestRule.WAIT_TIMEOUT_MS;
import android.annotation.SuppressLint;
+import android.content.Intent;
import android.os.Build;
import android.util.Pair;
@@ -1387,6 +1388,48 @@
}
}
+ @Test
+ @SmallTest
+ @Feature({"AndroidWebView"})
+ public void testNullContentsClientIntentLaunchFlagsRemoved() throws Throwable {
+ try {
+ // The test will fire real intents through the test activity.
+ // Need to temporarily suppress startActivity otherwise there will be a
+ // handler selection window and the test can't dismiss that.
+ mActivityTestRule.getActivity().setIgnoreStartActivity(true);
+ final String testUrl =
+ "intent://example.com/#Intent;scheme=http;launchFlags=0x10000003;end";
+ setupWithProvidedContentsClient(new TestDefaultContentsClient());
+ AwActivityTestRule.enableJavaScriptOnUiThread(mAwContents);
+ final String pageTitle = "Click Title";
+ final String htmlWithLink =
+ "<html><title>"
+ + pageTitle
+ + "</title>"
+ + "<body><a id='link' href='"
+ + testUrl
+ + "'>Click this!</a></body></html>";
+ final String urlWithLink =
+ mWebServer.setResponse(
+ "/html_with_link.html",
+ htmlWithLink,
+ CommonResources.getTextHtmlHeaders(true));
+
+ mActivityTestRule.loadUrlSync(
+ mAwContents, mContentsClient.getOnPageFinishedHelper(), urlWithLink);
+ JSUtils.clickNodeWithUserGesture(mAwContents.getWebContents(), "link");
+ mActivityTestRule.pollUiThread(
+ () -> mActivityTestRule.getActivity().getLastSentIntent() != null);
+ int flags = mActivityTestRule.getActivity().getLastSentIntent().getFlags();
+ Assert.assertEquals(0, flags & Intent.FLAG_GRANT_READ_URI_PERMISSION);
+ Assert.assertEquals(0, flags & Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
+ Assert.assertEquals(
+ Intent.FLAG_ACTIVITY_NEW_TASK, flags & Intent.FLAG_ACTIVITY_NEW_TASK);
+ } finally {
+ mActivityTestRule.getActivity().setIgnoreStartActivity(false);
+ }
+ }
+
private void setAppLinkPolicy(final AwPolicyProvider testProvider, String url) {
final PolicyData[] policies = {
new PolicyData.Str(sEnterpriseAuthAppLinkPolicy, "[{ \"url\": \"" + url + "\"}]")
Regression Test / PoC
diff --git a/android_webview/javatests/src/org/chromium/android_webview/test/AwContentsClientShouldOverrideUrlLoadingTest.java b/android_webview/javatests/src/org/chromium/android_webview/test/AwContentsClientShouldOverrideUrlLoadingTest.java
index b3e9a937..938f58a 100644
--- a/android_webview/javatests/src/org/chromium/android_webview/test/AwContentsClientShouldOverrideUrlLoadingTest.java
+++ b/android_webview/javatests/src/org/chromium/android_webview/test/AwContentsClientShouldOverrideUrlLoadingTest.java
@@ -8,6 +8,7 @@
import static org.chromium.android_webview.test.AwActivityTestRule.WAIT_TIMEOUT_MS;
import android.annotation.SuppressLint;
+import android.content.Intent;
import android.os.Build;
import android.util.Pair;
@@ -1387,6 +1388,48 @@
}
}
+ @Test
+ @SmallTest
+ @Feature({"AndroidWebView"})
+ public void testNullContentsClientIntentLaunchFlagsRemoved() throws Throwable {
+ try {
+ // The test will fire real intents through the test activity.
+ // Need to temporarily suppress startActivity otherwise there will be a
+ // handler selection window and the test can't dismiss that.
+ mActivityTestRule.getActivity().setIgnoreStartActivity(true);
+ final String testUrl =
+ "intent://example.com/#Intent;scheme=http;launchFlags=0x10000003;end";
+ setupWithProvidedContentsClient(new TestDefaultContentsClient());
+ AwActivityTestRule.enableJavaScriptOnUiThread(mAwContents);
+ final String pageTitle = "Click Title";
+ final String htmlWithLink =
+ "<html><title>"
+ + pageTitle
+ + "</title>"
+ + "<body><a id='link' href='"
+ + testUrl
+ + "'>Click this!</a></body></html>";
+ final String urlWithLink =
+ mWebServer.setResponse(
+ "/html_with_link.html",
+ htmlWithLink,
+ CommonResources.getTextHtmlHeaders(true));
+
+ mActivityTestRule.loadUrlSync(
+ mAwContents, mContentsClient.getOnPageFinishedHelper(), urlWithLink);
+ JSUtils.clickNodeWithUserGesture(mAwContents.getWebContents(), "link");
+ mActivityTestRule.pollUiThread(
+ () -> mActivityTestRule.getActivity().getLastSentIntent() != null);
+ int flags = mActivityTestRule.getActivity().getLastSentIntent().getFlags();
+ Assert.assertEquals(0, flags & Intent.FLAG_GRANT_READ_URI_PERMISSION);
+ Assert.assertEquals(0, flags & Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
+ Assert.assertEquals(
+ Intent.FLAG_ACTIVITY_NEW_TASK, flags & Intent.FLAG_ACTIVITY_NEW_TASK);
+ } finally {
+ mActivityTestRule.getActivity().setIgnoreStartActivity(false);
+ }
+ }
+
private void setAppLinkPolicy(final AwPolicyProvider testProvider, String url) {
final PolicyData[] policies = {
new PolicyData.Str(sEnterpriseAuthAppLinkPolicy, "[{ \"url\": \"" + url + "\"}]")
Original Bug Report
Potential sandbox escape in WebView via unsanitized Intent flags in sendBrowsingIntent
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 security team.
Overview: Android WebView’s default external navigation handler fails to sanitize Intent flags parsed from intent:// URIs. A malicious website can leverage this to grant a local attacker-controlled application read and write access to the host app’s private files. This potentially allows a full bypass of the Android application sandbox if the host app uses a FileProvider.
Affected files:
android_webview/java/src/org/chromium/android_webview/AwContentsClient.java
Estimated timestamp from git blame: 2025-06-20
Description
In Android WebView, if an embedding application does not set a custom WebViewClient, external navigations are handled by the default AwContentsClient.sendBrowsingIntent method. This method uses Intent.parseUri to construct an Android Intent from an intent:// URI.
While the method sanitizes the Intent’s component, category, and selector to prevent certain types of exploitation, it completely fails to sanitize the Intent’s flags. The Android framework’s Intent.parseUri explicitly parses the launchFlags fragment parameter and applies it to the intent.
By injecting flags such as Intent.FLAG_GRANT_READ_URI_PERMISSION (0x1) and Intent.FLAG_GRANT_WRITE_URI_PERMISSION (0x2), a malicious website can force the host application to fire an intent that delegates access to its own private content:// URIs (such as those exposed by a FileProvider). Because the Intent is fired from the host application’s Activity context, the Android OS correctly attributes the Intent to the host app and honors the URI permission grant, inadvertently giving the attacker-specified target application full access to the host’s private files.
Potential Attack Steps
Note: These are potential steps based on code analysis; a working Proof of Concept has not been executed.
- Preconditions: A victim application uses a
WebViewwithout setting a customWebViewClientand implements aFileProviderwithgrantUriPermissions="true"(a common configuration for sharing files). - Attacker Setup: The attacker publishes a malicious companion app (e.g.,
com.attacker.app) which the victim installs. - Execution: The victim visits the attacker’s malicious website inside the victim application’s WebView.
- Bypass User Gesture: The attacker’s server responds with an HTTP 302 redirect. This automatically bypasses the
hasUserGesturecheck inAwContentsClient.sendBrowsingIntent. - Crafted Payload: The redirect target is a crafted URI targeting the victim’s FileProvider:
intent://com.victim.fileprovider/internal_data.db#Intent;scheme=content;launchFlags=0x3;package=com.attacker.app;end - Privilege Escalation:
sendBrowsingIntentparses the URI, applyinglaunchFlags=0x3(Read and Write URI Permissions). It then callscontext.startActivity(intent)using the victim app’s context. - Data Compromise: The Android OS launches
com.attacker.app, explicitly granting it read and write permissions tointernal_data.db. The attacker app can now silently exfiltrate or modify the victim app’s private data, achieving a sandbox escape.
Suggested Fix
Implement Intent flag sanitization in AwContentsClient.sendBrowsingIntent before calling startActivity. This should mirror the protection already present in Chrome’s ExternalNavigationHandler.sanitizeQueryIntentActivitiesIntent, which masks flags against a safe allowlist:
intent.setFlags(intent.getFlags() & ALLOWED_INTENT_FLAGS);
The ALLOWED_INTENT_FLAGS list must strictly exclude URI permission granting flags (FLAG_GRANT_READ_URI_PERMISSION, FLAG_GRANT_WRITE_URI_PERMISSION, FLAG_GRANT_PERSISTABLE_URI_PERMISSION, and FLAG_GRANT_PREFIX_URI_PERMISSION).
Evaluated with Chrome root at commit: ff3d2b74fa39431785bd60e51463b08fcc71ee33
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.