Add flighted validation in WebView intent handling, Fixes AB#3674205#3170
Add flighted validation in WebView intent handling, Fixes AB#3674205#3170cacosta33 wants to merge 7 commits into
Conversation
Introduces a flight-gated validation step in the AAD WebView client's intent handling path. Default on; the flight provides a rollback path. Work item: https://dev.azure.com/IdentityDivision/Engineering/_workitems/edit/3674205 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Adds a flight-gated hardening step to the AAD WebView client’s broker-install intent:// handling so that (when enabled) only Google Play Store intents are launched, while preserving legacy behavior when the flight is disabled.
Changes:
- Added a new ECS-controlled flight
ENABLE_BROKER_INSTALL_INTENT_VALIDATION(default OFF). - In the
intent://broker-install handling path (flight ON only): clear explicit component/selector and block non-Play-Store target packages before launching. - Expanded unit test coverage for allow-list enforcement, component clearing, and legacy behavior; fixed a previously non-executing test by adding
@Test.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| common4j/src/main/com/microsoft/identity/common/java/flighting/CommonFlight.java | Adds the new flight definition (default OFF) to gate the new validation logic. |
| common/src/main/java/com/microsoft/identity/common/internal/ui/webview/AzureActiveDirectoryWebViewClient.java | Implements the flighted post-parse validation (clear component/selector + allow-list com.android.vending) before startActivity. |
| common/src/test/java/com/microsoft/identity/common/internal/ui/webview/AzureActiveDirectoryWebViewClientTest.java | Adds/updates intent-path tests to validate allow-list behavior, component clearing, and rollback behavior when flight is OFF. |
…idation Emits an OpenTelemetry span only when ENABLE_BROKER_INSTALL_INTENT_VALIDATION is on so the fix outcome (launched / blocked / error) can be confirmed in telemetry. Zero behavior change while the flight is off; all span usage is null-guarded. Adds one attribute (is_broker_install_intent_blocked) and a changelog entry. AB#3674205 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Restructure processIntentToInstallBrokerApp so the flight-off path is the original method (unchanged), and all validation + telemetry lives inside the flight-on branch with a single generic catch(Throwable). This removes the scattered null-guards and keeps zero behavior/telemetry impact when the flight is off. AB#3674205 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Resolve conflicts in changelog.txt, AzureActiveDirectoryWebViewClient.java, and AzureActiveDirectoryWebViewClientTest.java. Keep the flight-off path byte-for-byte identical to dev, preserve dev's onboarding telemetry hook (recordOnboardingStep before the flight check), and retain the flight-gated broker-install intent validation + span from PR #3170. AB#3674205 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
❌ Work item link check failed. Description does not contain AB#{ID}. Click here to Learn more. |
Restructure processIntentToInstallBrokerApp so the flight branch reads as an
explicit positive gate for reviewers:
if (ENABLE_BROKER_INSTALL_INTENT_VALIDATION) { NEW validation + span }
else { original dev try/catch, byte-for-byte unchanged }
Previously the method used a negated early-return
(if (!flag) { old; return; }) followed by the new code below, which moved the
original code and made the diff harder to review. The else branch now preserves
dev's original behavior verbatim (only re-indented), so flight-OFF behavior is
provably identical and all new, flighted logic/telemetry is isolated in the if
branch. No behavior change; small duplication of the original try/catch is
intentional for reviewer clarity.
AB#3674205
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
✅ Work item link check complete. Description contains link AB#3674205 to an Azure Boards work item. |
- Default unstubbed flights to their real default value in the WebView intent-validation test helper so tests don't silently depend on Mockito's boolean default as the WebViewClient evolves (Copilot review comment). - Reword the #3170 changelog entry to describe the security behavior (allow-list validation of broker-install intent targets) rather than only the telemetry span (melissaahn review comment). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
| // Clear any explicit component or selector carried by the parsed intent so that | ||
| // activity resolution is driven solely by the validated package. | ||
| intent.setComponent(null); | ||
| intent.setSelector(null); |
There was a problem hiding this comment.
we clear the selector here too, but I only see tests for the explicit-component case and the non-allowlisted package. nothing exercises a selector. that's kinda the trickier redirect vector - the routing gate and the new check both look at the base package, so setting base package = com.android.vending passes, and a SEL selector pointing at another app is what startActivity would actually resolve. can we add a case with a selector to a non-Play-Store target and assert it still doesn't launch?
| Logger.warn(methodTag, | ||
| "Blocking intent request to non-allow-listed package: " + targetPackage); | ||
| span.setAttribute(AttributeName.is_broker_install_intent_blocked.name(), true); | ||
| span.setStatus(StatusCode.OK); |
There was a problem hiding this comment.
on a block we set the span OK and just return, so handleUrl falls through to true and the webview keeps going with nothing launched and no completion callback. processOpenIdVcRequest does the opposite for its "no handler" case, StatusCode.ERROR + returnError. I'm guessing the silent drop here is deliberate (firing returnError would let an injected intent kill the auth session), just making sure the OK + continue is intentional and not defaulting to the happy path.
| * Package name of the Google Play Store, the legitimate launch target for a broker-install | ||
| * {@code intent://} request. | ||
| */ | ||
| private static final String GOOGLE_PLAY_STORE_PACKAGE_NAME = "com.android.vending"; |
There was a problem hiding this comment.
isIntentRequestToInstallBrokerApp still inlines this same value as ;package=com.android.vending; for its gate check. Could we build that substring off this constant so the gate and the allow-list can't drift apart later?
| return; | ||
| } | ||
|
|
||
| view.getContext().startActivity(intent); |
There was a problem hiding this comment.
we null the component and selector, but the rest of what the attacker put in the #Intent fragment still rides into startActivity - launchFlags, data uri, categories, extras. with the package pinned to com.android.vending it's not really a leak (a surviving FLAG_GRANT_READ_URI_PERMISSION would grant the store, not the page). the standard webview intent handling also masks off the FLAG_GRANT_* bits and adds CATEGORY_BROWSABLE before launching though. is pinning the package enough on its own here?
| if (CommonFlightsManager.INSTANCE.getFlightsProvider() | ||
| .isFlightEnabled(ENABLE_BROKER_INSTALL_INTENT_VALIDATION)) { | ||
| // Flight ON (new behavior): validate the intent target and record the outcome to a | ||
| // telemetry span so the fix's behavior (launched / blocked / error) can be confirmed from |
There was a problem hiding this comment.
Do we really need a dedicated span for something that runs within the webview and a small part of the overall acquire token interactive flows?
| * package was not the allow-listed store. Set on the {@link SpanName#ProcessBrokerInstallIntent} | ||
| * span (emitted only when the validation flight is enabled). | ||
| */ | ||
| is_broker_install_intent_blocked, |
There was a problem hiding this comment.
Looks like you are going to have just 1 attribute in the new span you are creating. This defeats the whole purpose of a span. Why not emit the attribute on the existing span?
Summary
Adds a flight-gated validation step to the AAD WebView client's broker-install
intent://handling path. When the flight is enabled, the parsed intent's component/selector are cleared and its target package must be the Google Play Store before the activity is launched. When the flight is disabled, behavior is byte-for-byte identical to today (kill-switch / rollback).Work item: https://dev.azure.com/IdentityDivision/Engineering/_workitems/edit/3674205
Feature flag & rollout
EnableBrokerInstallIntentValidation(CommonFlight), default OFF.commonships to broker + MSAL + downstream consumers, so this is intended to ramp progressively (e.g. 1% then 10% then 100%) while watching install-flow success telemetry.Changes
CommonFlight.java— newENABLE_BROKER_INSTALL_INTENT_VALIDATIONflight (defaultfalse).AzureActiveDirectoryWebViewClient.java— inside the flighted branch only: clear any explicit component/selector on the parsed intent and require the resolved package to be on the allow-list (com.android.vending) beforestartActivity.@Test(so it never ran).Validation
:common:testLocalDebugUnitTestfor the affected client passed (BUILD SUCCESSFUL), all tests pass (4 intent-path tests confirmed executing).Do not close the linked work item until confirmed
A couple of points cannot be verified from this library's code alone and may affect priority — please confirm before resolving the linked item (the code hardening here is safe to merge/ramp regardless):
AB#3674205
Notes for reviewers
OneAuthSharedFunctionsor any 1P IPC surface.