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 WebView
DescriptionInsufficient validation of untrusted input in WebView
ComponentWebView
Bug ClassLogic Error
Tracker498877660
Fix commitcfbbdab6693d (chromium/src) +57/-0
CISA KEVNot listed
CreditedGoogle
Disclosed2026-07-29

Files Changed

  • android_webview/java/src/org/chromium/android_webview/AwContentsClient.java
  • android_webview/javatests/src/org/chromium/android_webview/test/AwContentsClientShouldOverrideUrlLoadingTest.java
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 + "\"}]")
Loading diff…

Regression Test / PoC

shipped with the fix
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 + "\"}]")
Loading diff…

Original Bug Report

reported by vm...@google.com

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.

  1. Preconditions: A victim application uses a WebView without setting a custom WebViewClient and implements a FileProvider with grantUriPermissions="true" (a common configuration for sharing files).
  2. Attacker Setup: The attacker publishes a malicious companion app (e.g., com.attacker.app) which the victim installs.
  3. Execution: The victim visits the attacker’s malicious website inside the victim application’s WebView.
  4. Bypass User Gesture: The attacker’s server responds with an HTTP 302 redirect. This automatically bypasses the hasUserGesture check in AwContentsClient.sendBrowsingIntent.
  5. 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
  6. Privilege Escalation: sendBrowsingIntent parses the URI, applying launchFlags=0x3 (Read and Write URI Permissions). It then calls context.startActivity(intent) using the victim app’s context.
  7. Data Compromise: The Android OS launches com.attacker.app, explicitly granting it read and write permissions to internal_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.

View on issue tracker