Mobile Deep Links - Attack Surface and Pentesting Guide

Deep links are how the mobile web and native apps talk to each other. Tap a link in an email, get taken straight to a product page inside an app. Click a password reset link, the app opens to the right screen. From a user experience standpoint, it’s seamless. From a pentesting standpoint, it’s an input vector that routes untrusted external data directly into your app — often bypassing the normal flow of authentication and navigation.

This post covers how deep linking works across both platforms, where it breaks, and a concrete methodology for testing it on real engagements.

The Three Generations of Deep Linking

Understanding the evolution matters because you’ll encounter all three in production apps — sometimes all three at once.

Generation 1: Custom URL Schemes

The original deep linking mechanism. An app registers a custom URI scheme (e.g., twitter://, myapp://) in its manifest, and any link matching that scheme launches the app.

Android — declared in AndroidManifest.xml:

1
2
3
4
5
6
7
8
<activity android:name=".DeepLinkActivity">
    <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="myapp" android:host="action"/>
    </intent-filter>
</activity>

iOS — declared in Info.plist:

1
2
3
4
5
6
7
8
9
<key>CFBundleURLTypes</key>
<array>
    <dict>
        <key>CFBundleURLSchemes</key>
        <array>
            <string>myapp</string>
        </array>
    </dict>
</array>

The fundamental problem: Any app can register the same scheme. There is no global registry, no ownership verification. If two apps claim myapp://, the OS either shows a disambiguation dialog (Android) or picks one silently (iOS uses the last installed). An attacker’s app can claim your scheme.

The response to scheme hijacking. Instead of claiming an arbitrary scheme, the app claims ownership of a real HTTPS domain by placing a verification file on the server.

Apple Universal Links — server hosts apple-app-site-association at /.well-known/:

1
2
3
4
5
6
7
8
9
10
11
12
13
{
    "applinks": {
        "details": [
            {
                "appIDs": ["TEAMID.com.example.app"],
                "components": [
                    { "/": "/product/*", "comment": "Matches any product page" },
                    { "/": "/account/reset", "comment": "Password reset flow" }
                ]
            }
        ]
    }
}

Android App Links — server hosts assetlinks.json at /.well-known/:

1
2
3
4
5
6
7
8
[{
    "relation": ["delegate_permission/common.handle_all_urls"],
    "target": {
        "namespace": "android_app",
        "package_name": "com.example.app",
        "sha256_cert_fingerprints": ["AB:CD:EF:..."]
    }
}]

When a verified link is tapped, the OS doesn’t show a chooser — it routes directly to the claimed app. If verification fails, it falls back to the browser.

The key difference from custom schemes: The attacker would need to compromise the web server or the app signing key to perform a hijack.

The previous two require the app to already be installed. Deferred deep links solve the “not yet installed” case — a user clicks a link before installing the app, installs it, and on first launch the app reconstructs and acts on the original deep link.

This is typically implemented through third-party SDKs (Branch.io, Adjust, AppsFlyer) or Firebase Dynamic Links. The mechanism:

1
2
3
User clicks link → Third-party attribution server stores the link context →
User installs app → App contacts attribution server on first launch →
Server returns the deferred link → App navigates to the target

Security concern: The deferred link token (or fingerprint-based matching) is often stored in a way that can be intercepted or manipulated — intercepting it at the attribution SDK endpoint or spoofing the fingerprinting signal.

The Technical Routing Pipeline

Understanding what happens between “user taps link” and “app handles link” is essential for knowing where to insert test cases.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
User taps link
    ↓
OS evaluates: registered scheme? → YES → launch app, call:
    application(_:open:options:)       ← for URL schemes
    
    OR
    
Universal Link verified? → YES → launch app, call:
    application(_:continue:restorationHandler:)  ← for Universal Links
    scene(_:willConnectTo:options:)              ← for scene-based lifecycle
    ↓
App receives URL/NSUserActivity
    ↓
App parses host, path, query parameters
    ↓
App navigates/acts on the parsed data
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// URL Scheme handler (AppDelegate)
func application(_ app: UIApplication,
                 open url: URL,
                 options: [UIApplication.OpenURLOptionsKey: Any]) -> Bool {
    // url = myapp://product?id=12345
    guard let host = url.host,
          let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else {
        return false
    }
    let productId = components.queryItems?.first(where: { $0.name == "id" })?.value
    // ← This is your attack surface
    navigateToProduct(id: productId)
    return true
}

// Universal Link handler (AppDelegate)
func application(_ application: UIApplication,
                 continue userActivity: NSUserActivity,
                 restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
    guard userActivity.activityType == NSUserActivityTypeBrowsingWeb,
          let url = userActivity.webpageURL else { return false }
    // url.path, url.queryItems — same attack surface
    return handleUniversalLink(url: url)
}
1
2
3
4
5
6
7
8
9
10
11
User taps link
    ↓
Intent is created with action=VIEW, data=<URI>
    ↓
ActivityManager resolves matching intent-filter
    ↓
Target Activity's onCreate() / onNewIntent() receives the Intent
    ↓
Activity calls getIntent().getData() to get the URI
    ↓
Activity parses URI and acts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// In the receiving Activity
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    handleDeepLink(intent)
}

override fun onNewIntent(intent: Intent) {
    super.onNewIntent(intent)
    handleDeepLink(intent)  // also handle if Activity already running
}

private fun handleDeepLink(intent: Intent) {
    val data: Uri = intent.data ?: return
    val action = data.host         // "action" from myapp://action/detail?id=123
    val detail = data.lastPathSegment
    val id = data.getQueryParameter("id")  // ← attack surface
    // ...
}

1. Scheme Hijacking

Any app can claim your custom URL scheme. An attacker distributes a malicious app claiming mybank:// and waits for the victim to have both apps installed.

Attacker’s malicious app manifest:

1
2
3
4
5
6
7
<activity android:name=".CredentialHarvester">
    <intent-filter android:priority="999">  <!-- High priority to win resolution -->
        <action android:name="android.intent.action.VIEW"/>
        <category android:name="android.intent.category.BROWSABLE"/>
        <data android:scheme="mybank"/>
    </intent-filter>
</activity>

The victim’s browser sends mybank://login?token=abc123 — the attacker’s app intercepts it instead.

Test for this:

1
2
3
4
5
6
7
8
9
10
11
12
13
# Android: enumerate URL schemes the target app registers
aapt dump xmltree target.apk AndroidManifest.xml | grep -A10 "scheme"

# iOS: enumerate URL schemes from Info.plist
unzip app.ipa -d app_out
plutil -p app_out/Payload/AppName.app/Info.plist | grep -A5 CFBundleURLSchemes

# Simulate a deep link being sent to a specific app
adb shell am start -a android.intent.action.VIEW \
    -d "myapp://admin?bypass=true" com.target.app

adb shell am start -a android.intent.action.VIEW \
    -d "myapp://admin?bypass=true"   # ← no target: sees what wins the resolution

2. Missing Input Validation on URL Parameters

Deep link parameters are fully attacker-controlled. Any parameter passed via a deep link should be treated with the same skepticism as an HTTP query parameter — but developers often don’t.

Common vulnerable patterns:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Vulnerable: directly using deep link parameter as navigation target
val destination = intent.data?.getQueryParameter("redirect")
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(destination)))
// ← Open Redirect: "myapp://home?redirect=javascript:alert(1)"
// ← Or: "myapp://home?redirect=malicious://steal-tokens"

// Vulnerable: deep link parameter used in WebView URL
val urlToLoad = intent.data?.getQueryParameter("url")
webView.loadUrl(urlToLoad)
// ← JavaScript injection: "myapp://help?url=javascript:document.cookie"
// ← Arbitrary URL load

// Vulnerable: SQL or local database interaction driven by deep link param
val userId = intent.data?.getQueryParameter("user_id")
db.rawQuery("SELECT * FROM users WHERE id = $userId", null)
// ← SQL injection via deep link

Test for injection:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# Test open redirect via deep link
adb shell am start -a android.intent.action.VIEW \
    -d "myapp://home?redirect=https://attacker.com"

# Test WebView URL injection
adb shell am start -a android.intent.action.VIEW \
    -d "myapp://help?url=javascript:alert(document.cookie)"

# Test path traversal
adb shell am start -a android.intent.action.VIEW \
    -d "myapp://file?path=../../private/token.txt"

# iOS via simctl
xcrun simctl openurl booted "myapp://home?redirect=javascript:document.cookie"

Some apps implement deep link handling logic before confirming the user is authenticated. An unauthenticated deep link to a protected screen is a logic flaw.

1
2
3
4
5
6
7
8
// Vulnerable: handling deep link without checking auth state first
func application(_ app: UIApplication, open url: URL, options: ...) -> Bool {
    if url.host == "admin" {
        let vc = AdminViewController()  // ← No auth check!
        navigationController.pushViewController(vc, animated: false)
    }
    return true
}

Test methodology:

  1. Log out of the app completely
  2. From outside the app (browser, ADB shell, simctl), send a deep link to a protected/authenticated endpoint
  3. Observe if the app navigates to the protected content without authentication
1
2
3
4
# While app is logged out, attempt to deep link to authenticated area
xcrun simctl openurl booted "myapp://account/dashboard"
xcrun simctl openurl booted "myapp://admin/panel"
xcrun simctl openurl booted "myapp://transaction/history"

Deep links can trigger state-changing operations without user confirmation. If a sensitive action (e.g., adding a payment method, following a user, confirming a transaction) can be triggered purely by opening a URL, any website or app can send that link.

1
2
3
4
5
Attacker's webpage contains:
<meta http-equiv="refresh" content="0; url=myapp://payment/add?card=4111...">

Victim visits the page → Safari redirects to the deep link →
App adds the payment method without user interaction
1
2
3
4
5
6
# Test all state-changing deep link paths
adb shell am start -a android.intent.action.VIEW \
    -d "myapp://friend/add?user_id=attacker_id"

adb shell am start -a android.intent.action.VIEW \
    -d "myapp://payment/confirm?amount=999&to=attacker_account"

5. AASA / assetlinks.json Misconfiguration

For Universal Links and App Links to actually provide their security guarantees, the server-side verification file must be correct. Misconfigurations cause silent fallback to browser or (worse) allow circumvention.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# Check Android Digital Asset Links
curl https://example.com/.well-known/assetlinks.json
# Verify: package name, SHA-256 fingerprint match production cert
# Test: what happens with a URL that's NOT in the associated paths?

# Check Apple AASA
curl https://example.com/.well-known/apple-app-site-association
# Verify: correct appID, paths don't wildcard too broadly
# Dangerous: "paths": ["*"] — claims ALL paths on the domain

# Test path not claimed in AASA: does it fall back to browser safely?
xcrun simctl openurl booted "https://example.com/path-not-in-aasa"

# Test AASA for removed/rotated Team ID
# If the Team ID in AASA no longer matches the installed app's Team ID,
# Universal Links stop working — but the app may still handle fallback URLs

Common misconfiguration: over-broad path wildcarding

1
2
3
4
5
6
7
8
9
// Dangerous: claims the entire domain
"components": [{ "/": "/*" }]

// Better: only claim what the app actually handles
"components": [
    { "/": "/product/*" },
    { "/": "/account/reset" },
    { "/": "/checkout" }
]

SDKs like Branch.io use a short-lived token embedded in the link URL. If intercepted (via MITM, log leak, or referrer header), the token can be used to hijack the first-launch deep link experience.

1
2
3
4
5
6
7
# During a MITM proxy session (Burp Suite), capture the app's first-launch
# attribution API call:
# POST https://api2.branch.io/v1/open
# Response contains: { "data": { "+clicked_branch_link": true, "$deeplink_path": "..." } }

# The $deeplink_path value is what the app will navigate to on first launch
# If you can manipulate this response, you control first-launch navigation

Pentesting Methodology

Phase 1: Enumerate All Deep Link Entry Points

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Android: extract from APK directly
apktool d target.apk -o apk_decoded

# Find intent-filter declarations with scheme/host/path
grep -r "android:scheme" apk_decoded/
grep -r "android:host" apk_decoded/
grep -r "android:pathPattern" apk_decoded/
# Also look for string literals that look like deep links
strings apk_decoded/lib/arm64-v8a/libapp.so | grep -E "://[a-zA-Z0-9]"

# iOS: extract from IPA
unzip app.ipa -d app_out
plutil -p app_out/Payload/AppName.app/Info.plist | grep -A20 CFBundleURLTypes
plutil -p app_out/Payload/AppName.app/Info.plist | grep -A20 CFBundleURLSchemes
strings app_out/Payload/AppName.app/AppName | grep -E "[a-z]+://"

Phase 2: Map Parameters to Functionality

For each deep link you find, build a map of what parameters exist and what they control:

Deep Link Parameters Functionality Risk
myapp://product id, variant Navigate to product IDOR test
myapp://account/reset token Password reset Token replay/reuse
myapp://payment/authorize amount, merchant Trigger payment CSRF, parameter manipulation
myapp://webview url Load WebView Open redirect, JS injection
myapp://promo code Apply promo code Business logic abuse

Phase 3: Automated Parameter Fuzzing

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# Simple deep link fuzzer (using adb or simctl)
import subprocess
import sys

scheme = "myapp"
endpoint = "product"
param_name = "id"

payloads = [
    # IDOR
    "1", "0", "-1", "99999999", "OTHER_USER_ID",
    # Injection
    "' OR 1=1--", "1; DROP TABLE products",
    # Path traversal
    "../../../etc/passwd", "%2F..%2F..%2F",
    # Format strings
    "%s%s%s%s%s", "", "",
    # Very long value
    "A" * 2000,
    # Null byte
    "1%00admin",
]

for payload in payloads:
    url = f"{scheme}://{endpoint}?{param_name}={payload}"
    print(f"[*] Testing: {url}")
    result = subprocess.run([
        "adb", "shell", "am", "start",
        "-a", "android.intent.action.VIEW",
        "-d", url
    ], capture_output=True, text=True)
    # Observe app behavior for each — crashes, unexpected navigation, errors

Phase 4: Verify Server-Side Validation Files

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#!/bin/bash
# Deep link verification audit script

TARGET_DOMAIN="example.com"

echo "[*] Checking Android assetlinks.json"
curl -s "https://$TARGET_DOMAIN/.well-known/assetlinks.json" | python3 -m json.tool

echo ""
echo "[*] Checking iOS AASA"
curl -s "https://$TARGET_DOMAIN/.well-known/apple-app-site-association" | python3 -m json.tool

echo ""
echo "[*] Testing fallback behavior (URL not in claimed paths)"
# This should open in browser, not the app
xcrun simctl openurl booted "https://$TARGET_DOMAIN/path-definitely-not-claimed"

echo ""
echo "[*] Testing HTTP vs HTTPS (Universal Links require HTTPS)"
xcrun simctl openurl booted "http://$TARGET_DOMAIN/product/123"

Defense Recommendations

Vulnerability Fix
Scheme hijacking Migrate from custom URL schemes to Universal Links / App Links
Parameter injection Validate and sanitize all deep link parameters; use allowlists
Auth bypass Check authentication state before handling any deep link navigation
CSRF via deep link Require user confirmation for state-changing actions triggered by deep links
Open redirect Use a fixed internal navigation map; never use URL parameters as redirect targets
Overly broad AASA Explicitly enumerate only the paths your app handles
Deferred link interception Use HTTPS + certificate pinning for attribution SDK calls

[!TIP] The single most impactful improvement most apps can make: never trust the content of deep link parameters to determine privilege or skip authentication. The deep link is for navigation context — not authorization. All sensitive actions triggered by a deep link should re-verify the session server-side.


모바일 딥링크: 개념부터 공격 표면까지

딥링크는 모바일 웹과 네이티브 앱이 서로 연결되는 방식입니다. 이메일의 링크를 탭하면 앱의 특정 상품 페이지로 바로 이동하고, 비밀번호 재설정 링크를 클릭하면 앱이 열리면서 올바른 화면이 표시됩니다. 사용자 경험 관점에서는 매끄럽지만, 펜테스팅 관점에서는 외부에서 신뢰할 수 없는 데이터를 앱 내부로 직접 라우팅하는 입력 벡터입니다 — 종종 인증과 정상적인 네비게이션 흐름을 우회하면서.

딥링크의 세 세대

이 진화를 이해하는 것이 중요한 이유는 실제 프로덕션 앱에서 세 가지 방식을 모두 마주칠 수 있기 때문입니다 — 때로는 동시에.

1세대: 커스텀 URL 스킴

원형 딥링킹 메커니즘. 앱이 커스텀 URI 스킴(예: twitter://, myapp://)을 매니페스트에 등록하고, 해당 스킴과 일치하는 링크가 앱을 실행합니다.

근본적인 문제: 어떤 앱이든 동일한 스킴을 등록할 수 있습니다. 전역 레지스트리도 소유권 검증도 없습니다. 두 앱이 myapp://를 사용한다면 OS는 선택 다이얼로그를 표시하거나 조용히 하나를 선택합니다. 공격자의 앱이 여러분의 스킴을 가로챌 수 있습니다.

2세대: 검증된 딥링크 (유니버셜 링크 / 앱 링크)

스킴 하이재킹에 대한 대응책. 임의의 스킴을 주장하는 대신 서버에 검증 파일을 배치하여 실제 HTTPS 도메인의 소유권을 주장합니다.

핵심 차이점: 공격자가 하이재킹을 수행하려면 웹 서버를 침해하거나 앱 서명 키를 탈취해야 합니다.

이전 두 방식은 앱이 이미 설치되어 있어야 합니다. 지연 딥링크는 “아직 설치하지 않은” 경우를 해결합니다 — 앱 설치 전에 링크를 클릭한 사용자가 앱을 설치하고 첫 실행 시 원래 딥링크를 복원하여 해당 대상으로 이동합니다.

보안 우려: 지연 링크 토큰(또는 핑거프린트 기반 매칭)은 종종 가로채거나 조작할 수 있는 방식으로 저장됩니다.

기술적 라우팅 파이프라인

“사용자가 링크를 탭한다”에서 “앱이 링크를 처리한다”까지 사이에 무슨 일이 일어나는지 이해해야 어디에 테스트 케이스를 삽입해야 하는지 알 수 있습니다.

Android 딥링크 처리 흐름

1
2
3
4
5
6
7
8
9
10
11
사용자가 링크 탭
    ↓
action=VIEW, data=<URI>로 Intent 생성
    ↓
ActivityManager가 일치하는 intent-filter 검색
    ↓
대상 Activity의 onCreate() / onNewIntent()가 Intent 수신
    ↓
Activity가 getIntent().getData()로 URI 획득
    ↓
URI 파싱 후 처리
1
2
3
4
5
private fun handleDeepLink(intent: Intent) {
    val data: Uri = intent.data ?: return
    val action = data.host
    val id = data.getQueryParameter("id")  // ← 여기가 공격 표면
}

공격 표면: 딥링크가 취약해지는 지점

1. 스킴 하이재킹

모든 앱이 여러분의 커스텀 URL 스킴을 등록할 수 있습니다. 공격자가 mybank://를 사용하는 악성 앱을 배포하면, 피해자가 두 앱을 모두 설치한 경우 인증 토큰을 가로챌 수 있습니다.

테스트 방법:

1
2
3
4
5
6
7
8
9
10
# 대상 앱이 등록한 URL 스킴 열거
aapt dump xmltree target.apk AndroidManifest.xml | grep -A10 "scheme"

# 특정 앱을 대상으로 딥링크 전송
adb shell am start -a android.intent.action.VIEW \
    -d "myapp://admin?bypass=true" com.target.app

# 앱 지정 없이 전송 (어떤 앱이 스킴을 가져가는지 확인)
adb shell am start -a android.intent.action.VIEW \
    -d "myapp://admin?bypass=true"

2. URL 파라미터 입력 검증 누락

딥링크 파라미터는 완전히 공격자가 제어합니다. HTTP 쿼리 파라미터와 동일한 수준의 의심을 가져야 하지만, 개발자들은 종종 그렇게 하지 않습니다.

취약한 패턴들:

1
2
3
4
5
6
7
8
9
10
11
// 취약: 딥링크 파라미터를 네비게이션 대상으로 직접 사용 (오픈 리다이렉트)
val destination = intent.data?.getQueryParameter("redirect")
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(destination)))

// 취약: 딥링크 파라미터가 WebView URL로 사용 (JS 인젝션)
val urlToLoad = intent.data?.getQueryParameter("url")
webView.loadUrl(urlToLoad)

// 취약: 딥링크로 SQL 구성 (SQL 인젝션)
val userId = intent.data?.getQueryParameter("user_id")
db.rawQuery("SELECT * FROM users WHERE id = $userId", null)

인젝션 테스트:

1
2
3
4
5
6
7
8
9
10
11
# 오픈 리다이렉트 테스트
adb shell am start -a android.intent.action.VIEW \
    -d "myapp://home?redirect=https://attacker.com"

# WebView URL 인젝션
adb shell am start -a android.intent.action.VIEW \
    -d "myapp://help?url=javascript:alert(document.cookie)"

# 경로 탐색 (Path Traversal)
adb shell am start -a android.intent.action.VIEW \
    -d "myapp://file?path=../../private/token.txt"

3. 딥링크를 통한 인증 우회

일부 앱은 사용자 인증 확인 전에 딥링크 처리 로직을 실행합니다. 보호된 화면으로의 비인증 딥링크는 로직 결함입니다.

테스트 방법:

  1. 앱에서 완전히 로그아웃
  2. 앱 외부(브라우저, ADB 셸, simctl)에서 보호된 엔드포인트로 딥링크 전송
  3. 인증 없이 보호된 콘텐츠로 이동하는지 관찰
1
2
3
4
# 로그아웃 상태에서 인증 영역으로 딥링크 시도
xcrun simctl openurl booted "myapp://account/dashboard"
xcrun simctl openurl booted "myapp://admin/panel"
xcrun simctl openurl booted "myapp://transaction/history"

4. 딥링크를 통한 CSRF

딥링크는 사용자 확인 없이 상태 변경 작업을 트리거할 수 있습니다. 민감한 작업(결제 수단 추가, 팔로우, 거래 확인)이 URL을 여는 것만으로 트리거된다면, 어떤 웹사이트나 앱이든 해당 링크를 보낼 수 있습니다.

1
2
3
4
5
공격자의 웹페이지:
<meta http-equiv="refresh" content="0; url=myapp://payment/add?card=4111...">

피해자가 페이지 방문 → Safari가 딥링크로 리다이렉트 →
사용자 상호작용 없이 앱이 결제 수단 추가

5. AASA / assetlinks.json 잘못된 구성

유니버셜 링크와 앱 링크의 보안 보장을 위해서는 서버 측 검증 파일이 올바르게 구성되어야 합니다.

1
2
3
4
5
6
7
8
9
10
# Android Digital Asset Links 확인
curl https://example.com/.well-known/assetlinks.json
# 패키지명, SHA-256 지문이 프로덕션 인증서와 일치하는지 확인

# Apple AASA 확인
curl https://example.com/.well-known/apple-app-site-association
# 지나치게 광범위한 와일드카드 확인: "paths": ["*"]는 위험

# AASA에 없는 경로 테스트 (브라우저 폴백이 안전한지)
xcrun simctl openurl booted "https://example.com/path-not-in-aasa"

펜테스팅 방법론

1단계: 모든 딥링크 진입점 열거

1
2
3
4
5
6
7
8
9
10
# Android: APK에서 직접 추출
apktool d target.apk -o apk_decoded
grep -r "android:scheme" apk_decoded/
grep -r "android:host" apk_decoded/
strings apk_decoded/lib/arm64-v8a/libapp.so | grep -E "://[a-zA-Z0-9]"

# iOS: IPA에서 추출
unzip app.ipa -d app_out
plutil -p app_out/Payload/AppName.app/Info.plist | grep -A20 CFBundleURLTypes
strings app_out/Payload/AppName.app/AppName | grep -E "[a-z]+://"

2단계: 파라미터와 기능 매핑

발견한 각 딥링크에 대해 어떤 파라미터가 있고 무엇을 제어하는지 목록을 작성합니다:

딥링크 파라미터 기능 위험
myapp://product id, variant 상품 페이지 이동 IDOR 테스트
myapp://account/reset token 비밀번호 재설정 토큰 재사용
myapp://payment/authorize amount, merchant 결제 트리거 CSRF, 파라미터 조작
myapp://webview url WebView 로드 오픈 리다이렉트, JS 인젝션
myapp://promo code 프로모션 코드 적용 비즈니스 로직 남용

3단계: 자동화된 파라미터 퍼징

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import subprocess

scheme = "myapp"
endpoint = "product"
param_name = "id"

payloads = [
    # IDOR
    "1", "0", "-1", "99999999", "OTHER_USER_ID",
    # 인젝션
    "' OR 1=1--", "1; DROP TABLE products",
    # 경로 탐색
    "../../../etc/passwd", "%2F..%2F..%2F",
    # 매우 긴 값
    "A" * 2000,
]

for payload in payloads:
    url = f"{scheme}://{endpoint}?{param_name}={payload}"
    print(f"[*] 테스트 중: {url}")
    subprocess.run([
        "adb", "shell", "am", "start",
        "-a", "android.intent.action.VIEW", "-d", url
    ])
    # 각 페이로드에 대한 앱 동작 관찰 (충돌, 예상치 못한 네비게이션, 오류)

방어 권장 사항

취약점 해결책
스킴 하이재킹 커스텀 URL 스킴에서 유니버셜 링크 / 앱 링크로 마이그레이션
파라미터 인젝션 모든 딥링크 파라미터를 검증하고 살균; 허용 목록 사용
인증 우회 딥링크 네비게이션 처리 전 인증 상태 확인
딥링크를 통한 CSRF 딥링크로 트리거된 상태 변경 작업에 사용자 확인 요구
오픈 리다이렉트 고정된 내부 네비게이션 맵 사용; URL 파라미터를 리다이렉트 대상으로 사용 금지
과도하게 광범위한 AASA 앱이 실제로 처리하는 경로만 명시적으로 열거
지연 링크 가로채기 어트리뷰션 SDK 호출에 HTTPS + 인증서 피닝 적용

[!TIP] 대부분의 앱이 적용할 수 있는 가장 영향력 있는 개선 사항은 딥링크 파라미터 내용으로 권한을 결정하거나 인증을 건너뛰지 않는 것입니다. 딥링크는 네비게이션 컨텍스트를 위한 것이지, 권한 부여를 위한 것이 아닙니다. 딥링크로 트리거된 모든 민감한 작업은 서버 측에서 세션을 재검증해야 합니다.