diff --git a/android/brave_java_sources.gni b/android/brave_java_sources.gni index 3b948fe34e1..e1bb77b9eb9 100644 --- a/android/brave_java_sources.gni +++ b/android/brave_java_sources.gni @@ -32,6 +32,7 @@ brave_java_sources = [ "../../brave/android/java/org/chromium/chrome/browser/BraveFeatureUtil.java", "../../brave/android/java/org/chromium/chrome/browser/BraveHelper.java", "../../brave/android/java/org/chromium/chrome/browser/BraveIntentHandler.java", + "../../brave/android/java/org/chromium/chrome/browser/BraveIntentHandlerInternal.java", "../../brave/android/java/org/chromium/chrome/browser/BraveLaunchIntentDispatcher.java", "../../brave/android/java/org/chromium/chrome/browser/BraveLocalState.java", "../../brave/android/java/org/chromium/chrome/browser/BraveRelaunchUtils.java", diff --git a/android/java/apk_for_test.flags b/android/java/apk_for_test.flags index f149c928e95..7e998cd788c 100644 --- a/android/java/apk_for_test.flags +++ b/android/java/apk_for_test.flags @@ -701,6 +701,7 @@ *** getUrlForWebapp(...); *** isJavascriptSchemeOrInvalidUrl(...); *** extractUrlFromIntent(...); + *** intentHasUnsafeInternalScheme(...); } -keep class org.chromium.chrome.browser.BraveIntentHandler { diff --git a/android/java/org/chromium/chrome/browser/BraveIntentHandler.java b/android/java/org/chromium/chrome/browser/BraveIntentHandler.java index 570cdaeeb20..0efeb0a9f8c 100644 --- a/android/java/org/chromium/chrome/browser/BraveIntentHandler.java +++ b/android/java/org/chromium/chrome/browser/BraveIntentHandler.java @@ -20,12 +20,15 @@ import org.chromium.chrome.browser.search_engines.TemplateUrlServiceFactory; import org.chromium.chrome.browser.searchwidget.SearchWidgetProvider; import org.chromium.content_public.browser.BrowserStartupController; +import java.util.Locale; import java.util.concurrent.Callable; @NullMarked public class BraveIntentHandler { private static final String TAG = "BraveIntentHandler"; + private static final String BRAVE_SCHEME = "brave"; + /** An extra to indicate that the intent was triggered from an app widget Leo button. */ public static final String EXTRA_INVOKED_FROM_APP_WIDGET_LEO = "com.android.brave.invoked_from_app_widget_leo"; @@ -146,4 +149,22 @@ public class BraveIntentHandler { assert false; return false; } + + /** + * Bytecode-redirected from {@link IntentHandler#intentHasUnsafeInternalScheme}. Defers to the + * upstream check (which handles chrome://, chrome-native://, devtools://, distiller://, + * about://) and additionally blocks brave://, since it is a display alias for chrome:// and + * gets rewritten to chrome:// deeper in the navigation stack — too late to protect this guard. + */ + public static boolean intentHasUnsafeInternalScheme( + @Nullable String scheme, @Nullable String url, Intent intent) { + if (BraveIntentHandlerInternal.intentHasUnsafeInternalScheme(scheme, url, intent)) { + return true; + } + return scheme != null + && BRAVE_SCHEME.equals(scheme.toLowerCase(Locale.US)) + && (intent.hasCategory(Intent.CATEGORY_BROWSABLE) + || intent.hasCategory(Intent.CATEGORY_DEFAULT) + || intent.getCategories() == null); + } } diff --git a/android/java/org/chromium/chrome/browser/BraveIntentHandlerInternal.java b/android/java/org/chromium/chrome/browser/BraveIntentHandlerInternal.java new file mode 100644 index 00000000000..341ad674fef --- /dev/null +++ b/android/java/org/chromium/chrome/browser/BraveIntentHandlerInternal.java @@ -0,0 +1,28 @@ +/* Copyright (c) 2026 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at https://mozilla.org/MPL/2.0/. */ + +package org.chromium.chrome.browser; + +import android.content.Intent; + +import org.chromium.build.annotations.NullMarked; +import org.chromium.build.annotations.Nullable; + +/** + * Holds same-named stubs that bytecode-redirect to private members of {@link IntentHandler}, so + * {@link BraveIntentHandler} can back-call them despite Java visibility rules. The stub bodies are + * never executed: {@code BraveIntentHandlerClassAdapter} rewrites each call site to invoke the + * upstream method (which it also bumps to public at bytecode time). + */ +@NullMarked +final class BraveIntentHandlerInternal { + private BraveIntentHandlerInternal() {} + + static boolean intentHasUnsafeInternalScheme( + @Nullable String scheme, @Nullable String url, Intent intent) { + assert false; + return false; + } +} diff --git a/android/javatests/org/chromium/chrome/browser/BytecodeTest.java b/android/javatests/org/chromium/chrome/browser/BytecodeTest.java index 4a0dcd4ebea..5931b150f94 100644 --- a/android/javatests/org/chromium/chrome/browser/BytecodeTest.java +++ b/android/javatests/org/chromium/chrome/browser/BytecodeTest.java @@ -808,6 +808,15 @@ public class BytecodeTest { MethodModifier.STATIC, String.class, Intent.class)); + Assert.assertTrue( + methodExists( + "org/chromium/chrome/browser/IntentHandler", + "intentHasUnsafeInternalScheme", + MethodModifier.STATIC, + boolean.class, + String.class, + String.class, + Intent.class)); Assert.assertTrue( methodExists( "org/chromium/chrome/browser/download/dialogs/DownloadLocationDialogCoordinator", // presubmit: ignore-long-line diff --git a/android/junit/src/org/chromium/chrome/browser/BraveIntentHandlerUnitTest.java b/android/junit/src/org/chromium/chrome/browser/BraveIntentHandlerUnitTest.java index bdcbd55bdc9..9012ffa240f 100644 --- a/android/junit/src/org/chromium/chrome/browser/BraveIntentHandlerUnitTest.java +++ b/android/junit/src/org/chromium/chrome/browser/BraveIntentHandlerUnitTest.java @@ -6,6 +6,8 @@ package org.chromium.chrome.browser; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; import android.content.Intent; import android.net.Uri; @@ -57,4 +59,34 @@ public class BraveIntentHandlerUnitTest { assertEquals("https://example.com/search?q=test&source=android", result); } + + @Test + @SmallTest + public void intentHasUnsafeInternalScheme_braveScheme_isBlocked() { + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.addCategory(Intent.CATEGORY_BROWSABLE); + assertTrue( + BraveIntentHandler.intentHasUnsafeInternalScheme( + "brave", "brave://flags/", intent)); + } + + @Test + @SmallTest + public void intentHasUnsafeInternalScheme_braveScheme_mixedCase_isBlocked() { + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.addCategory(Intent.CATEGORY_DEFAULT); + assertTrue( + BraveIntentHandler.intentHasUnsafeInternalScheme( + "Brave", "Brave://flags/", intent)); + } + + @Test + @SmallTest + public void intentHasUnsafeInternalScheme_httpsScheme_isAllowed() { + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.addCategory(Intent.CATEGORY_BROWSABLE); + assertFalse( + BraveIntentHandler.intentHasUnsafeInternalScheme( + "https", "https://example.com/", intent)); + } } diff --git a/build/android/bytecode/java/org/brave/bytecode/BraveIntentHandlerClassAdapter.java b/build/android/bytecode/java/org/brave/bytecode/BraveIntentHandlerClassAdapter.java index a1dcc39ab43..5275e128647 100644 --- a/build/android/bytecode/java/org/brave/bytecode/BraveIntentHandlerClassAdapter.java +++ b/build/android/bytecode/java/org/brave/bytecode/BraveIntentHandlerClassAdapter.java @@ -10,6 +10,8 @@ import org.objectweb.asm.ClassVisitor; public class BraveIntentHandlerClassAdapter extends BraveClassVisitor { static String sIntentHandlerClassName = "org/chromium/chrome/browser/IntentHandler"; static String sBraveIntentHandlerClassName = "org/chromium/chrome/browser/BraveIntentHandler"; + static String sBraveIntentHandlerInternalClassName = + "org/chromium/chrome/browser/BraveIntentHandlerInternal"; public BraveIntentHandlerClassAdapter(ClassVisitor visitor) { super(visitor); @@ -27,5 +29,15 @@ public class BraveIntentHandlerClassAdapter extends BraveClassVisitor { changeMethodOwner( sIntentHandlerClassName, "extractUrlFromIntent", sBraveIntentHandlerClassName); + + makePublicMethod(sIntentHandlerClassName, "intentHasUnsafeInternalScheme"); + changeMethodOwner( + sIntentHandlerClassName, + "intentHasUnsafeInternalScheme", + sBraveIntentHandlerClassName); + changeMethodOwner( + sBraveIntentHandlerInternalClassName, + "intentHasUnsafeInternalScheme", + sIntentHandlerClassName); } }