Skip to content

Commit

Permalink
Don't launch external fallback URLs when not allowed to launch extern…
Browse files Browse the repository at this point in the history
…al URLs

Ensure that we don't launch external fallback URLs, when we've otherwise
decided that we should not launch an external navigation (even if it was
possible to). Also prevent auto subframe navigation from launching
external fallback URLs.

Bug: 1094442
Change-Id: I3828a0957c160c6ed2c2a38b54cf54af9b797327
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2248124
Reviewed-by: Yaron Friedman <yfriedman@chromium.org>
Reviewed-by: Robert Sesek <rsesek@chromium.org>
Commit-Queue: Michael Thiessen <mthiesse@chromium.org>
Cr-Commit-Position: refs/heads/master@{#779574}
  • Loading branch information
Michael Thiessen authored and Commit Bot committed Jun 18, 2020
1 parent 8d6bf7c commit 37b5b74
Show file tree
Hide file tree
Showing 5 changed files with 199 additions and 63 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@ public class UrlOverridingTest {
BASE_PATH + "navigation_to_cct_via_intent_uri.html";
private static final String NAVIGATION_TO_FILE_SCHEME_FROM_INTENT_URI =
BASE_PATH + "navigation_to_file_scheme_via_intent_uri.html";
private static final String SUBFRAME_REDIRECT_WITH_PLAY_FALLBACK =
BASE_PATH + "subframe_navigation_with_play_fallback.html";

private static class TestTabObserver extends EmptyTabObserver {
private final CallbackHelper mFinishCallback;
Expand Down Expand Up @@ -512,4 +514,12 @@ public void testIntentURIWithEmptySchemeDoesNothing() throws TimeoutException {
String originalUrl = mTestServer.getURL(NAVIGATION_TO_FILE_SCHEME_FROM_INTENT_URI);
loadUrlAndWaitForIntentUrl(originalUrl, true, false, false, null, false, "empty_scheme");
}
}

@Test
@LargeTest
public void testSubframeLoadCannotLaunchPlayApp() throws TimeoutException {
mActivityTestRule.startMainActivityOnBlankPage();
loadUrlAndWaitForIntentUrl(
mTestServer.getURL(SUBFRAME_REDIRECT_WITH_PLAY_FALLBACK), false, false);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<html>
<head>
<meta http-equiv="refresh" content="0;URL='intent://test/#Intent;scheme=test;package=not.real.package;S.browser_fallback_url=https%3A%2F%2Fplay.google.com%2Fstore%2Fapps%2Fdetails%3Fid%3Dcom.android.chrome;end'" />
</head>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<!DOCTYPE html>
<html>
<body>
<iframe src="frame_redirect_with_play_fallback.html"></iframe>
</body>
</html>
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,17 @@ public class ExternalNavigationHandler {
@VisibleForTesting
static final String INTENT_ACTION_HISTOGRAM = "Android.Intent.OverrideUrlLoadingIntentAction";

// Helper class to return a boolean by reference.
private static class MutableBoolean {
private Boolean mValue = null;
public void set(boolean value) {
mValue = value;
}
public Boolean get() {
return mValue;
}
}

private final ExternalNavigationDelegate mDelegate;

/**
Expand Down Expand Up @@ -217,9 +228,16 @@ public ExternalNavigationHandler(ExternalNavigationDelegate delegate) {
&& !UrlUtilities.isValidForIntentFallbackNavigation(browserFallbackUrl)) {
browserFallbackUrl = null;
}

// TODO(https://crbug.com/1096099): Refactor shouldOverrideUrlLoadingInternal, splitting it
// up to separate out the notions wanting to fire an external intent vs being able to.
MutableBoolean canLaunchExternalFallbackResult = new MutableBoolean();

long time = SystemClock.elapsedRealtime();
@OverrideUrlLoadingResult
int result = shouldOverrideUrlLoadingInternal(params, targetIntent, browserFallbackUrl);
int result = shouldOverrideUrlLoadingInternal(
params, targetIntent, browserFallbackUrl, canLaunchExternalFallbackResult);
assert canLaunchExternalFallbackResult.get() != null;
RecordHistogram.recordTimesHistogram(
"Android.StrictMode.OverrideUrlLoadingTime", SystemClock.elapsedRealtime() - time);

Expand All @@ -236,31 +254,64 @@ public ExternalNavigationHandler(ExternalNavigationDelegate delegate) {
&& (params.getRedirectHandler() == null
// For instance, if this is a chained fallback URL, we ignore it.
|| !params.getRedirectHandler().shouldNotOverrideUrlLoading())) {
result = handleFallbackUrl(params, targetIntent, browserFallbackUrl);
result = handleFallbackUrl(params, targetIntent, browserFallbackUrl,
canLaunchExternalFallbackResult.get());
}
if (DEBUG) printDebugShouldOverrideUrlLoadingResult(result);
return result;
}

private @OverrideUrlLoadingResult int handleFallbackUrl(
ExternalNavigationParams params, Intent targetIntent, String browserFallbackUrl) {
private @OverrideUrlLoadingResult int handleFallbackUrl(ExternalNavigationParams params,
Intent targetIntent, String browserFallbackUrl, boolean canLaunchExternalFallback) {
if (mDelegate.isIntentToInstantApp(targetIntent)) {
RecordHistogram.recordEnumeratedHistogram("Android.InstantApps.DirectInstantAppsIntent",
AiaIntent.FALLBACK_USED, AiaIntent.NUM_ENTRIES);
}
// Launch WebAPK if it can handle the URL.
try {
Intent intent = Intent.parseUri(browserFallbackUrl, Intent.URI_INTENT_SCHEME);
sanitizeQueryIntentActivitiesIntent(intent);
List<ResolveInfo> resolvingInfos = queryIntentActivities(intent);
if (!isAlreadyInTargetWebApk(resolvingInfos, params)
&& launchWebApkIfSoleIntentHandler(resolvingInfos, intent)) {
return OverrideUrlLoadingResult.OVERRIDE_WITH_EXTERNAL_INTENT;

if (canLaunchExternalFallback) {
if (shouldBlockAllExternalAppLaunches(params) || params.isIncognito()) {
throw new SecurityException("Context is not allowed to launch an external app.");
}
} catch (Exception e) {
if (DEBUG) Log.i(TAG, "Could not parse fallback url as intent");
// Launch WebAPK if it can handle the URL.
try {
Intent intent = Intent.parseUri(browserFallbackUrl, Intent.URI_INTENT_SCHEME);
sanitizeQueryIntentActivitiesIntent(intent);
List<ResolveInfo> resolvingInfos = queryIntentActivities(intent);
if (!isAlreadyInTargetWebApk(resolvingInfos, params)
&& launchWebApkIfSoleIntentHandler(resolvingInfos, intent)) {
return OverrideUrlLoadingResult.OVERRIDE_WITH_EXTERNAL_INTENT;
}
} catch (Exception e) {
if (DEBUG) Log.i(TAG, "Could not parse fallback url as intent");
}

// If the fallback URL is a link to Play Store, send the user to Play Store app
// instead: crbug.com/638672.
Pair<String, String> appInfo = maybeGetPlayStoreAppIdAndReferrer(browserFallbackUrl);
if (appInfo != null) {
String marketReferrer = TextUtils.isEmpty(appInfo.second)
? ContextUtils.getApplicationContext().getPackageName()
: appInfo.second;
return sendIntentToMarket(appInfo.first, marketReferrer, params);
}
}

// For subframes, we don't support fallback url for now.
// http://crbug.com/364522.
if (!params.isMainFrame()) {
if (DEBUG) Log.i(TAG, "Don't support fallback url in subframes");
return OverrideUrlLoadingResult.NO_OVERRIDE;
}
return clobberCurrentTabWithFallbackUrl(browserFallbackUrl, params);

// NOTE: any further redirection from fall-back URL should not override URL loading.
// Otherwise, it can be used in chain for fingerprinting multiple app installation
// status in one shot. In order to prevent this scenario, we notify redirection
// handler that redirection from the current navigation should stay in this app.
if (params.getRedirectHandler() != null) {
params.getRedirectHandler().setShouldNotOverrideUrlLoadingOnCurrentRedirectChain();
}
if (DEBUG) Log.i(TAG, "clobberCurrentTab called");
return clobberCurrentTab(browserFallbackUrl, params.getReferrerUrl());
}

private void printDebugShouldOverrideUrlLoadingResult(int result) {
Expand Down Expand Up @@ -299,6 +350,19 @@ private boolean resolversSubsetOf(List<ResolveInfo> infos, List<ResolveInfo> con
return true;
}

/**
* https://crbug.com/1094442: Don't allow any external navigation on AUTO_SUBFRAME navigation
* (eg. initial ad frame navigation).
*/
private boolean blockExternalNavFromAutoSubframe(ExternalNavigationParams params) {
int pageTransitionCore = params.getPageTransition() & PageTransition.CORE_MASK;
if (pageTransitionCore == PageTransition.AUTO_SUBFRAME) {
if (DEBUG) Log.i(TAG, "Auto navigation in subframe");
return true;
}
return false;
}

/**
* http://crbug.com/441284 : Disallow firing external intent while the app is in the background.
*/
Expand Down Expand Up @@ -965,15 +1029,20 @@ params, targetIntent, browserFallbackUrl, isGoogleReferrer())) {
return false;
}

// Check if we're navigating under conditions that should never launch an external app.
private boolean shouldBlockAllExternalAppLaunches(ExternalNavigationParams params) {
return blockExternalNavFromAutoSubframe(params) || blockExternalNavWhileBackgrounded(params)
|| blockExternalNavFromBackgroundTab(params) || ignoreBackForwardNav(params);
}

private @OverrideUrlLoadingResult int shouldOverrideUrlLoadingInternal(
ExternalNavigationParams params, Intent targetIntent,
@Nullable String browserFallbackUrl) {
@Nullable String browserFallbackUrl, MutableBoolean canLaunchExternalFallbackResult) {
sanitizeQueryIntentActivitiesIntent(targetIntent);
// Don't allow external fallback URLs by default.
canLaunchExternalFallbackResult.set(false);

if (blockExternalNavWhileBackgrounded(params) || blockExternalNavFromBackgroundTab(params)
|| ignoreBackForwardNav(params)) {
return OverrideUrlLoadingResult.NO_OVERRIDE;
}
if (shouldBlockAllExternalAppLaunches(params)) return OverrideUrlLoadingResult.NO_OVERRIDE;

if (handleWithAutofillAssistant(params, targetIntent, browserFallbackUrl)) {
return OverrideUrlLoadingResult.NO_OVERRIDE;
Expand Down Expand Up @@ -1055,6 +1124,10 @@ params, targetIntent, browserFallbackUrl, isGoogleReferrer())) {

if (hasIntentScheme) recordIntentActionMetrics(targetIntent);

// From this point on, we have determined it is safe to launch an External App from a
// fallback URL, provided the user isn't in incognito.
if (!params.isIncognito()) canLaunchExternalFallbackResult.set(true);

Intent debugIntent = new Intent(targetIntent);
List<ResolveInfo> resolvingInfos = queryIntentActivities(targetIntent);
if (resolvingInfos.isEmpty()) {
Expand Down Expand Up @@ -1182,44 +1255,6 @@ private void sanitizeQueryIntentActivitiesIntent(Intent intent) {
}
}

/**
* Clobber the current tab with fallback URL.
*
* @param browserFallbackUrl The fallback URL.
* @param params The external navigation params.
* @return {@link OverrideUrlLoadingResult} if the tab was clobbered, or we launched an
* intent.
*/
private @OverrideUrlLoadingResult int clobberCurrentTabWithFallbackUrl(
String browserFallbackUrl, ExternalNavigationParams params) {
// If the fallback URL is a link to Play Store, send the user to Play Store app
// instead: crbug.com/638672.
Pair<String, String> appInfo = maybeGetPlayStoreAppIdAndReferrer(browserFallbackUrl);
if (appInfo != null) {
String marketReferrer = TextUtils.isEmpty(appInfo.second)
? ContextUtils.getApplicationContext().getPackageName()
: appInfo.second;
return sendIntentToMarket(appInfo.first, marketReferrer, params);
}

// For subframes, we don't support fallback url for now.
// http://crbug.com/364522.
if (!params.isMainFrame()) {
if (DEBUG) Log.i(TAG, "Don't support fallback url in subframes");
return OverrideUrlLoadingResult.NO_OVERRIDE;
}

// NOTE: any further redirection from fall-back URL should not override URL loading.
// Otherwise, it can be used in chain for fingerprinting multiple app installation
// status in one shot. In order to prevent this scenario, we notify redirection
// handler that redirection from the current navigation should stay in this app.
if (params.getRedirectHandler() != null) {
params.getRedirectHandler().setShouldNotOverrideUrlLoadingOnCurrentRedirectChain();
}
if (DEBUG) Log.i(TAG, "clobberCurrentTab called");
return clobberCurrentTab(browserFallbackUrl, params.getReferrerUrl());
}

/**
* If the given URL is to Google Play, extracts the package name and referrer tracking code
* from the {@param url} and returns as a Pair in that order. Otherwise returns null.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -943,18 +943,54 @@ public void testFallbackUrl_FallbackToMarketApp() {
.expecting(OverrideUrlLoadingResult.OVERRIDE_WITH_CLOBBERING_TAB, IGNORE);
}

@Test
@SmallTest
public void testFallbackUrl_SubframeFallbackToMarketApp() {
mDelegate.setCanResolveActivityForExternalSchemes(false);

RedirectHandler redirectHandler = RedirectHandler.create();
redirectHandler.updateNewUrlLoading(PageTransition.LINK, false, true, 0, 0);
String intent = "intent:///name/nm0000158#Intent;scheme=imdb;package=com.imdb.mobile;"
+ "S." + ExternalNavigationHandler.EXTRA_BROWSER_FALLBACK_URL + "="
+ "https://play.google.com/store/apps/details?id=com.imdb.mobile"
+ "&referrer=mypage;end";
checkUrl(intent)
.withIsMainFrame(false)
.withHasUserGesture(true)
.withRedirectHandler(redirectHandler)
.withPageTransition(PageTransition.LINK)
.expecting(OverrideUrlLoadingResult.OVERRIDE_WITH_EXTERNAL_INTENT,
START_OTHER_ACTIVITY);
Assert.assertEquals("market://details?id=com.imdb.mobile&referrer=mypage",
mDelegate.startActivityIntent.getDataString());

redirectHandler = RedirectHandler.create();
redirectHandler.updateNewUrlLoading(PageTransition.LINK, false, true, 0, 0);
String intentBadUrl = "intent:///name/nm0000158#Intent;scheme=imdb;package=com.imdb.mobile;"
+ "S." + ExternalNavigationHandler.EXTRA_BROWSER_FALLBACK_URL + "="
+ "https://play.google.com/store/search?q=pub:imdb;end";
checkUrl(intentBadUrl)
.withIsMainFrame(false)
.withHasUserGesture(true)
.withRedirectHandler(redirectHandler)
.withPageTransition(PageTransition.LINK)
.expecting(OverrideUrlLoadingResult.NO_OVERRIDE, IGNORE);
}

@Test
@SmallTest
public void testFallbackUrl_RedirectToIntentToMarket() {
mDelegate.setCanResolveActivityForExternalSchemes(false);

RedirectHandler redirectHandler = RedirectHandler.create();

redirectHandler.updateNewUrlLoading(PageTransition.TYPED, false, false, 0, 0);
redirectHandler.updateNewUrlLoading(PageTransition.LINK, false, true, 0, 0);
checkUrl("http://goo.gl/abcdefg")
.withPageTransition(PageTransition.TYPED)
.withPageTransition(PageTransition.LINK)
.withRedirectHandler(redirectHandler)
.expecting(OverrideUrlLoadingResult.NO_OVERRIDE, IGNORE);

redirectHandler.updateNewUrlLoading(PageTransition.LINK, false, false, 0, 0);
redirectHandler.updateNewUrlLoading(PageTransition.LINK, true, true, 0, 0);
String realIntent = "intent:///name/nm0000158#Intent;scheme=imdb;package=com.imdb.mobile;"
+ "S." + ExternalNavigationHandler.EXTRA_BROWSER_FALLBACK_URL + "="
+ "https://play.google.com/store/apps/details?id=com.imdb.mobile"
Expand All @@ -971,6 +1007,44 @@ public void testFallbackUrl_RedirectToIntentToMarket() {
mDelegate.startActivityIntent.getDataString());
}

@Test
@SmallTest
public void testFallbackUrl_DontFallbackForAutoSubframe() {
// IMDB app isn't installed.
mDelegate.setCanResolveActivityForExternalSchemes(false);

mDelegate.add(new IntentActivity(IMDB_WEBPAGE_FOR_TOM_HANKS, WEBAPK_PACKAGE_NAME)
.withIsWebApk(true));

RedirectHandler redirectHandler = RedirectHandler.create();
redirectHandler.updateNewUrlLoading(PageTransition.AUTO_SUBFRAME, true, true, 0, 0);

checkUrl(INTENT_URL_WITH_FALLBACK_URL)
.withIsMainFrame(false)
.withHasUserGesture(true)
.withRedirectHandler(redirectHandler)
.withPageTransition(PageTransition.AUTO_SUBFRAME)
.withReferrer(SEARCH_RESULT_URL_FOR_TOM_HANKS)
.expecting(OverrideUrlLoadingResult.NO_OVERRIDE, IGNORE);
}

@Test
@SmallTest
public void testFallbackUrl_NoExternalFallbackWithoutGesture() {
mDelegate.add(new IntentActivity(IMDB_WEBPAGE_FOR_TOM_HANKS, WEBAPK_PACKAGE_NAME)
.withIsWebApk(true));

RedirectHandler redirectHandler = RedirectHandler.create();
redirectHandler.updateNewUrlLoading(PageTransition.LINK, false, false, 0, 0);

checkUrl(INTENT_URL_WITH_FALLBACK_URL)
.withHasUserGesture(false)
.withRedirectHandler(redirectHandler)
.withPageTransition(PageTransition.LINK)
.withReferrer(SEARCH_RESULT_URL_FOR_TOM_HANKS)
.expecting(OverrideUrlLoadingResult.OVERRIDE_WITH_CLOBBERING_TAB, IGNORE);
}

@Test
@SmallTest
public void testFallbackUrl_IntentResolutionFailsWithoutPackageName() {
Expand Down Expand Up @@ -2259,6 +2333,7 @@ private class ExternalNavigationTestParams {
private boolean mHasUserGesture;
private RedirectHandler mRedirectHandler;
private boolean mIsRendererInitiated;
private boolean mIsMainFrame = true;

private ExternalNavigationTestParams(String url) {
mUrl = url;
Expand Down Expand Up @@ -2311,6 +2386,11 @@ public ExternalNavigationTestParams withIsRendererInitiated(boolean isRendererIn
return this;
}

public ExternalNavigationTestParams withIsMainFrame(boolean isMainFrame) {
mIsMainFrame = isMainFrame;
return this;
}

public void expecting(
@OverrideUrlLoadingResult int expectedOverrideResult, int otherExpectation) {
boolean expectStartIncognito = (otherExpectation & START_INCOGNITO) != 0;
Expand All @@ -2331,7 +2411,7 @@ public void expecting(
.setApplicationMustBeInForeground(mChromeAppInForegroundRequired)
.setRedirectHandler(mRedirectHandler)
.setIsBackgroundTabNavigation(mIsBackgroundTabNavigation)
.setIsMainFrame(true)
.setIsMainFrame(mIsMainFrame)
.setNativeClientPackageName(mDelegate.getReferrerWebappPackageName())
.setHasUserGesture(mHasUserGesture)
.setIsRendererInitiated(mIsRendererInitiated)
Expand Down

0 comments on commit 37b5b74

Please sign in to comment.