POSTS
Android App Links autoVerify=false Allowed Hijacking Authentication Flows
- 11 minutes read - 2206 wordsResearch is a constant process of failure and iteration. However, in most cases, you only see the one-in-a-thousand (successful) attempt. To normalize f*ck ups, and because I believe the behavior we identified in the course of this research is still relevant and interesting, this post is published for educational purposes.
Implementing secure Single-Sign-On (SSO) flows on mobile platforms is a continuos challenge. This post discusses an Android feature which potentially enabled a malicious Android app to hijack arbitrary SSO flows. As the feature existed on platform level (prior Android 12), it affected not only misconfigured apps, but also (web-)applications that follow OAuth best current practice1.
The vulnerability was reported to Google via the Android and Google Devices Security Reward Program on November, 29th 2024. Shortly after submission, Google highlighted a crucial thing that was missed before: Due to major rework of the App Link behavior, the reported issues do only work on Android versions prior to Android 12.
This research resulted from a collaboration of @kun_19 and me. Late November 2024 @kun_19 reached out to me with a question (paraphrased):
How can a mobile app OAuth flow that uses custom URL schemes be protected? Especially, in case the benign app is not installed on the victim’s (Android) device, given that PKCE does not help against
code
leaks either?
My initial recommendation was something like:
Use App Links / Universal Links as they rely on DNS.
Little did I know, as @kun_19 then nudged me to the autoVerify
Intent Filter property on Android2. Learning about such a powerful feature gadget, I then raised the question:
What would prevent me then from registering any callback URL as intent filter? Even from SSO flows that do not have associated mobile apps?
You may already guess it: The answer to that question will be answered in this blog post.
Disclaimer: During our tests, we only tested on our (outdated) physical test devices with Android 10 and Android 11. After reporting the issue to Google, they replied:
Thank you for your submission. We have taken an initial look, but we require additional information in order to accurately assess the impact of this report. We appreciate the provided in-depth report. However, Android R and Pixel Sunfish are no longer supported. Could you please provide and updated PoC and reproduction steps that demonstrate the issue on a supported Android version (1) and device (2)?
And after we confirmed that the last vulnerable version is Android 11:
Researcher Feedback: Thank you for your submission. Since this issue only affects Android versions that are no longer in support, this issue is being classified as NSBC (Not Security Bulletin Class).
So, it was definitely our fault to not verify the results on Android versions newer than Android 11.
Table of Contents
- Foundations
- Scenarios
- Real-World Example: Spotify
- A Note Regarding RFC 7636: Proof Key for Code Exchange
- Impact and CVSS
- Recommendation and Fix
- Mitigating Measures
- Conclusion
Foundations
When implementing OAuth/OIDC SSO flows nowadays, it is common practice to utilize Universal Links / App Links2 as redirect_uri
.
This approach is used, because when using custom URL schemes for OAuth 2.0 callbacks3 4, any application can register arbitrary URL schemes including the ones legitimate apps use for their mobile login flows. So, there is the clear assumption that to use such a link, there is a requirement of an active linking and verification of ownership for a given domain.
This recommendation is also in alignment with Fett et. al’s blog post5 from late 2020:
While redirecting from app2app and app2web can be secured really well on Android, it is difficult to secure the web2app redirection. There are two essential problems. First, Android App Links are only supported by the Chrome browser and second, it is not possible to set the certificate hash of the target app in the intent scheme. If either of these problems would be solved, we could safely redirect from the web to an app. So at the moment there are two options: Either the user is only allowed to use the Chrome browser (which is not possible if he starts the flow in another browser) or we have to accept the risk that the redirection could get hijacked by an app that was installed from an alternative app store with the same package name.
To solve this, we strongly recommend that alternate browsers are enhanced to support app links in the same way Chrome does.
This assumption is contradicted by the android:autoVerify
attribute, which allows to essentially bypass that binding, as by disabling verification, it is possible to trigger the target app selection dialog (disambiguation dialog) and to get a malicious app as suggestion for arbitrary navigation to URLs.
As a result, it is possible to hijack arbitrary redirect_uri
s by simply registering the target scheme, domain, and path within the malicious app’s manifest file:
<application>
<activity android:name=”MainActivity”>
<intent-filter android:autoVerify="false">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="http" />
<data android:scheme="https" />
<data android:host="www.example.com" />
<data android:path="/oauth/callback" />
</intent-filter>
</activity>
</application>
In case of a login flow that utilizes https://www.example.com/oauth/callback
to transfer the code
, this would lead to disclosure of the sensitive OAuth single-use credential.
Scenarios
There are multiple scenarios regarding a malicious attacker-controlled app that registers an intent filter for a victim domain that needs to be evaluated.
All examples used in the following assume that there is an attacker-controlled app that registers an intent filter for https://bughunters.google.com
with android:autoVerify="false"
:
<application>
<activity android:name=”MainActivity”>
<intent-filter android:autoVerify="false">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="http" />
<data android:scheme="https" />
<data android:host="bughunters.google.com" />
<data android:path="/" />
</intent-filter>
</activity>
</application>
Scenario 1: SSO Flow, No Legitimate App
In this scenario, we consider the case that there is no app that would legitimately register an intent filter.
Given that, we assume there is already a malicious app installed on the device, once there is a login flow and consequent top-level navigation to the redirect_uri=https://bughunters.google.com
, sensitive OAuth credentials (code
or access_token
depending on the flow) are sent to the attacker-controlled app.
- Install the example app.
- Navigate to https://x.lhq.at/p/674873ecbb8a1 with your Chrome mobile browser.
- Click the link
- Choose “Test App”:
- Observe that the app receives a “code”:
Scenario 2: SSO Flow, Legitimate App
In this scenario, we consider different cases where there is a legitimate app that registers an intent filter for the target domain.
Scenario 2.1: Benign App installed, autoVerify=true, universal Redirect URI defined
This is the only “secure” scenario. In case there is a benign app installed with autoVerify=true
set in its manifest for the target domain and the one existent redirect_uri
for all platforms (mobile + web) is correctly defined, the benign app is automatically opened by the OS (without disambiguation dialog).
Scenario 2.2: Benign App installed, autoVerify=true, different Redirect URIs
In some cases, the redirect_uri
of the mobile App (e.g. https://example.com/oauth/mobile/redirect) differs from the redirect_uri
of the corresponding web app (e.g. https://example.com/oauth/web/redirect). In this scenario, if the user is opening the web app in their mobile browser and is logging in, a malicious app that is claiming the redirect_uri
https://example.com/oauth/web/redirect (with autoVerify=false
) can intercept the OAuth credentials. Even if the benign app is installed and autoVerify=true
, the user gets a prompt to select a target app, because no domain verification takes place for https://example.com/oauth/web/redirect and in case the user selects the malicious app, the sensitive OAuth credentials are sent to the malicious app (see scenario 1).
Scenario 2.3: Benign App installed, autoVerify=false or not set
In this scenario, the user gets a prompt that includes all available apps installed that may handle the requested intent:
As a result, in case the end-user selects the malicious app, the sensitive OAuth credentials are sent to the malicious app (see scenario 1).
Scenario 2.4: Benign App NOT installed
In case only the malicious app is installed, there is still a prompt where the end-user has to select the target app. If this user interaction is performed, the OAuth “code” is again sent to the attacker via the malicious app.
Real-World Example: Spotify
Spotify follows current best practices and publishes a .well-known/assetlinks.json
for Android as well as a .well-known/apple-app-site-association
for iOS.
- https://accounts.spotify.com/.well-known/assetlinks.json
- https://accounts.spotify.com/.well-known/apple-app-site-association
Further, it registers several App Links via its Manifest file including android:autoVerify="true"
:
<activity android:theme="@style/Theme.Glue.Launcher" android:label="@string/app_name" android:icon="@mipmap/ic_launcher" android:name="com.spotify.music.SpotifyMainActivity" android:exported="true" android:launchMode="singleTask" android:screenOrientation="unspecified" android:configChanges="keyboardHidden" android:windowSoftInputMode="adjustNothing">
...
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:host="accounts.spotify.com"/>
<data android:scheme="https"/>
<data android:pathPrefix="/login/ott/music"/>
</intent-filter>
...
Based on our test and as described in scenario 2.2, it is still possible that a malicious app hijacks a login flow, even in case the benign Spotify app is installed.
Steps to reproduce:
- (Optional) Install the benign Spotify app on your device.
- Install the malicious app via
adb install malicious-spotify-debug.apk
. - Navigate to
https://spotify.com
in your mobile Chrome browser and start the login flow via Google. - Notice that when completing the login flow, there is an app selection dialog that asks where to open the URL. Choose the malicious app from step 2.
- Observe that the app receives a “code”:
A Note Regarding RFC 7636: Proof Key for Code Exchange
The Proof Key for Code Exchange (PKCE) mechanism was introduced to prevent CSRF and authorization code injection attacks. It introduces a binding between Auth Request and Token Request by adding a code_challenge
and a code_verifier
parameter.
A malicious actor who is able to forge an Auth Request and choose the code_challenge
on their own can bypass this protection, as they can compute a (code_challenge
,code_verifier
tuple on their own and use it to launch a CSRF-style “Cross-App” attack.
Impact and CVSS
CVSS: 7.4 (High)
https://www.first.org/cvss/calculator/3.0#CVSS:3.0/AV:L/AC:H/PR:N/UI:R/S:C/C:H/I:H/A:N
Confidentiality (
C:H
): A malicious actor who is able to obtain a beareraccess_token
can perform arbitrary actions on behalf of the associated end-user and in scope of the token, possibly including accessing restrict information. This poses a threat to the confidentiality of this information.Integrity (
I:H
): Likewise, depending on the scope of the token, the integrity of accessible resources can be threatened.Attack Vector (
AV:L
): A malicious app needs to be present on the victim’s device.Attack Complexity (
AC:H
): A successful attack depends on conditions out of the attacker’s control, namely that the victim user chooses the malicious app within the selection screen.User Interaction (
UI:R
): The user needs to actively select the malicious app.
Recommendation and Fix
In our original report, we added this recommendation:
It is recommended to deprecate and remove the
autoVerify=false
App Link verification mechanism. Like on other platforms such as iOS, only verified links should be considered by the OS.
In fact, this sums up pretty well how Google finally addressed the issue. This is how Google describes the current behavior in their developer documentation:
Note: On apps that target Android 12, the system makes several changes to how Android App Links are verified. These changes improve the reliability of the app-linking experience and provide more control to app developers and end users. You can manually invoke domain verification to test the reliability of your declarations.
Mitigating Measures
Besides these Platform changes, we identified additional defense-in-depth measures that appear to be reasonable, even on current Android versions (newer than Android 12). Relying parties are recommended to enforce POST-based SSO login flows. The described attack scenarios rely on GET-based redirects or top-level navigations that automatically invoke intents to native apps. POST-requests are handled differently in that regard, resulting in no credentials being disclosed to the attacker app.
- OAuth/OIDC: It is recommended to use the
response_mode=form_post
6 - SAML: It is recommended to use POST Bindings 7
Additionally, it is recommended to carefully evaluate the use of prompt=none
flows. prompt=login
or prompt=consent
should be preferred, as these flows enable the end-user to perform informed decisions and to cancel suspicious login flows before credentials are leaked to unintended parties.
Conclusion
To conclude, the autoVerify=false
feature of intent filters introduced a significant risk for all applications (both web and mobile) that rely on credential transfer via HTTP redirects or top-level navigations. Most importantly, this affected the majority of Single Sign-On (SSO) login flows. Strikingly, web applications without a companion mobile app could be considered the most affected, as their login flow could potentially be hijacked on Android devices despite following current best practices for SSO login flows, while having only limited options for mitigation.
With the changes introduced in Android 12, the security of App Links was significantly increased. Nevertheless, it is recommended to disable seamless login flows (prompt=none
) and use POST-based response modes if possible.
Thank you for reading this post!
Follow @kun_19 on Twitter and LinkedIn.
If you have any feedback, feel free to reach out via BlueSky, Mastodon, Twitter or LinkedIn. 👨💻
You can directly tweet about this post using this link. 🤓