Deep-Link ATO in Smart-Home Companion App, Identity Provider Session Hijack on Connected Web Properties
The Risk
A single tap on a normal-looking web link, sent by email, social post, or text, was enough to hand the attacker a logged-in session on the victim's account across the company's web sites. With that session the attacker could read the victim's full profile (email, name, address country, account identifiers), change profile fields, and trigger a real password-reset email to the victim's inbox. No password, no app install, no permissions, and no further interaction from the victim were needed.
The Vulnerability
Four chained weaknesses in the Android companion app turned a single tap from any browser into a full identity-provider session hijack on the company's web properties.
1. Exported activity with a public app-link filter
A compose host activity was declared android:exported="true" with no permission requirement. Its intent filters included an HTTPS app link on a publicly-resolvable provisioning host, which let the browser route an intent:// URL straight into the activity without an app picker.
2. Untrusted destination deserialized from intent extras
The activity read a destination string extra and deserialized it via a sealed-class serializer keyed on the fully-qualified class name. The attacker chose the discriminator (LegacyFragmentDestination) and supplied attacker-controlled componentType and JSON payload fields.
3. Component registry exposed a generic WebView component
For componentType = "webview", the registry returned a fragment that loaded the URL out of the JSON payload. There was no allowlist on which component types could be reached from external navigation.
4. Native host check did a substring match
Before granting the WebView an SSO auth code, the app called a host-allowlist check implemented in a native library. The native function performed a case-sensitive substring match against a list of country-coded brand domains. Any hostname containing one of those tokens passed, including a legitimately-registered domain that ended with the brand token, served over a real Let's Encrypt certificate.
The Attack
The delivery surface was any web page the victim opened in the mobile browser. The page contained a single link with an intent:// URL that the browser routed into the exported activity along with the attacker's destination extra.
- The victim taps the link in the mobile browser
- The browser dispatches the intent into the exported activity, with the attacker's
destinationextra andFLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_CLEAR_TASKset - The activity deserializes the destination, picks the WebView component, and calls into the WebView fragment
- The host check passes because the attacker's hostname contains the brand token
- The app fetches an identity-provider auth code and appends it to the URL as a query parameter, then loads the URL
- The attacker's HTTPS origin receives the auth code in the query string and forwards it to a headless browser
- The headless browser navigates to the brand's real web property with
?authCode=...&auth_action=sso.login. The identity provider's client SDK consumes the grant and sets logged-in session cookies for the victim
End-to-end timing was under ten seconds from tap to a fully-authenticated session on the victim's account, captured live on a researcher-owned account.
The Impact
Using the hijacked session, the following primitives were confirmed against a researcher-owned test account.
Full profile read
The identity provider's profile API returned email, first name, last name, country, internal account identifier, customer-relationship contact ID, linked login identities, consent records, and stored preferences.
Persistent profile write
Profile fields including first name, last name, country, locale, and phone number could be overwritten. A test write of firstName="PWNED" persisted and survived re-reads.
Password-reset email trigger
The identity provider's password-reset API accepted the victim's verified email and dispatched a real reset email. Combined with any inbox-access primitive, this chains into full password takeover.
Signed identity token issuance
The provider's JWT issuance API returned a short-lived signed token bound to the victim's account, usable as a bearer credential against any service that trusted those tokens.
Authenticated navigation as the victim
Account, support, and case-management web pages on the brand's domains rendered as the victim, with full session cookies set.
Remediation
- Add a signature-level permission to the exported compose host activity, or move the destination-deserializing path behind an internal-only activity that only accepts same-process callers
- Strip any
destinationextra from intents that arrive via a public intent filter. Public filters should only deliver URI data, not attacker-supplied serialized objects - Replace the substring host check with a strict equality match against a hard-coded list of fully-qualified hostnames
- Validate
componentTypeagainst a strict allowlist and exclude generic WebView components from values reachable through external navigation - Gate the SSO auth-code fetch on the explicit list of brand hosts the app actually bootstraps into, not on any host that contains the brand token
- Bind issued auth codes to the expected target origin server-side, so even client-side SDK consumption respects the intended redirect allowlist