Skip to content

Commit e6193ab

Browse files
h0tak88rclaude
andcommitted
feat(apkauditor): detect Jetpack Navigation deep-link Intent-extra injection
Adds a rule + correlation check for the forced-navigation bug class reported in YWH-PGM7888-591 (BookBeat): an exported activity hosts a NavHostFragment, and NavController.handleDeepLink() trusts the android-support-nav:controller: deepLinkIds/deepLinkArgs/deepLinkExtras Intent extras from whatever launches it -- letting any co-installed app force navigation to an arbitrary destination by numeric ID, bypassing login/profile gating (CWE-284). - ANDROID_RULES: new 'nav_deeplink_ids' pattern rule matches the Navigation library's deep-link extra key constants and NavController.handleDeepLink( call sites in decompiled class content. - Correlation step (after manifest + dex analysis, before grouping): if that rule fired AND the app declares an exported activity without a permission, push one consolidated, named finding ('nav_deeplink_forced_navigation') -- listing the exposed activities and manual verification/PoC steps (apktool + nav-graph enumeration + adb cold-start test), mirroring the report's own remediation guidance. - Verified with a functional test: a decompiled-content sample mirroring the report's evidence fires the new rule, and the correlation filter correctly isolates only the truly exported+unprotected activity from a mixed list. Note: this flags the pattern for manual verification (matching this rule engine's existing heuristic style) -- it does not walk res/navigation/*.xml to enumerate destinations/build a PoC the way the report's companion tool does; that would need AXMLParser support for arbitrary res/navigation XML, tracked as a possible follow-up. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent b4d27a9 commit e6193ab

1 file changed

Lines changed: 26 additions & 0 deletions

File tree

internal/api/ui/apkauditor/src/core/engine.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -830,6 +830,11 @@ const ANDROID_RULES = [
830830
patterns: [/getIntent\(\)\.getData\(\)\.get(?:Host|Path|Query|Fragment|Scheme)/g, /getIntent\(\)\.getData\(\)\.toString/g],
831831
description: 'URI data from deep link intent read without validation. Can enable open redirect, SSRF, or account takeover if used in navigation or API calls.', cwe: 'CWE-601', owasp: 'M1', masvs: 'PLATFORM-3'
832832
},
833+
{
834+
id: 'nav_deeplink_ids', name: 'Jetpack Navigation Deep-Link Intent-Extra Handling', severity: 'issue',
835+
patterns: [/android-support-nav:controller:deepLink(?:Ids|Args|Extras)/g, /NavController[\s\S]{0,60}?\.handleDeepLink\s*\(/g, /findNavController\s*\([^)]*\)[\s\S]{0,60}?\.handleDeepLink\s*\(/g],
836+
description: 'App uses Jetpack Navigation’s Intent-extra deep-link mechanism (NavController.handleDeepLink). The deepLinkIds/deepLinkArgs/deepLinkExtras extras are trusted from whatever Intent reaches the NavHost’s activity — they are not scoped to the app’s own intent-filter data URIs. If an exported activity hosts this NavController without stripping these extras first, any co-installed app can force navigation to arbitrary destinations by their numeric resource ID, bypassing intended start-destination gating (e.g. login/profile screens). See "Jetpack Navigation Deep-Link Injection" below if this app also declares an exported activity without a permission.', cwe: 'CWE-284', owasp: 'M1', masvs: 'PLATFORM-3'
837+
},
833838
{
834839
id: 'dynamic_receiver', name: 'Dynamic Broadcast Receiver (Potentially Exported)', severity: 'issue',
835840
patterns: [/registerReceiver\s*\([^)]*(?:new\s+IntentFilter|filter)/g],
@@ -3102,6 +3107,27 @@ async function analyzeAPK(arrayBuffer, fileMeta, opts) {
31023107
} catch (e) { R.warnings.push('Failed to scan ' + path + ': ' + (e.message || e)); }
31033108
}
31043109

3110+
// Correlate Jetpack Navigation deep-link handling with an exported, unprotected
3111+
// activity: this is the exact "forced navigation via deep-link Intent extras"
3112+
// primitive (CWE-284) — a malicious co-installed app can pass
3113+
// android-support-nav:controller:deepLinkIds/deepLinkArgs/deepLinkExtras to
3114+
// force navigation to arbitrary destinations, bypassing session/login gating.
3115+
if (R.findings.some(f => f.ruleId === 'nav_deeplink_ids')) {
3116+
const exposedActivities = (R.components && R.components.activities || []).filter(a => a.exported && !a.permission);
3117+
if (exposedActivities.length > 0) {
3118+
const names = exposedActivities.map(a => a.name).slice(0, 5).join(', ') + (exposedActivities.length > 5 ? `, +${exposedActivities.length - 5} more` : '');
3119+
R.findings.push({
3120+
ruleId: 'nav_deeplink_forced_navigation',
3121+
ruleName: 'Jetpack Navigation Deep-Link Injection (Forced Navigation) — Verify Manually',
3122+
severity: 'issue',
3123+
description: `This app both uses Jetpack Navigation's deep-link Intent-extra mechanism (NavController.handleDeepLink) AND declares exported activities without a permission: ${names}. If any of these hosts the NavHostFragment, a malicious co-installed app can send an Intent with the "android-support-nav:controller:deepLinkIds" extra set to a destination's numeric resource ID and force navigation there directly — bypassing the graph's start destination (commonly a login/profile gate) and potentially rendering authenticated-only screens while logged out. Verify: decompile with apktool, confirm the activity's layout declares a NavHostFragment/FragmentContainerView, enumerate destination IDs in res/navigation/*.xml, then test with 'adb shell am start -n <pkg>/<activity> --eia "android-support-nav:controller:deepLinkIds" <id>' after a cold start (force-stop first — onNewIntent() often re-guards the warm-start path). Fix: strip android-support-nav:controller:* extras from the launch Intent before the NavController initializes, or re-check session state on entry to sensitive destinations instead of relying on the start destination alone.`,
3124+
cwe: 'CWE-284', owasp: 'M1', masvs: 'PLATFORM-1',
3125+
file: 'AndroidManifest.xml + classes.dex', line: null,
3126+
match: `${exposedActivities.length} exported activit${exposedActivities.length === 1 ? 'y' : 'ies'} + Jetpack Navigation deep-link handling`,
3127+
});
3128+
}
3129+
}
3130+
31053131
onProgress(90, 'Detecting trackers');
31063132
R.trackers = detectTrackers(allDexStrings, R.files);
31073133

0 commit comments

Comments
 (0)