Deep links in financial apps are high-value targets. A compromised deep link could redirect a payment to the wrong recipient, expose account data, or trick a user into authorizing a fraudulent transaction. This guide covers the specific security measures fintech apps should implement for deep links.
For general deep link security, see Deep Linking Security: Preventing Hijacking and Abuse. For fintech compliance, see Fintech Compliance and Deep Links.
Threat Model
1. Link Hijacking
A malicious app registers to handle the same URL scheme as your app. When the user taps a deep link, the malicious app opens instead of yours.
Mitigation: Use Universal Links (iOS) and App Links (Android) exclusively. These verify domain ownership through AASA and assetlinks.json, preventing other apps from claiming your URLs. Never use custom URL schemes (yourapp://) for financial flows.
2. Parameter Tampering
An attacker modifies the deep link parameters before the user taps it:
Original: https://go.yourapp.com/pay?to=merchant_abc&amount=42.50
Tampered: https://go.yourapp.com/pay?to=attacker_xyz&amount=42.50
Mitigation: Use signed URLs. Include an HMAC signature that covers all critical parameters:
function createSignedPaymentLink(recipient, amount, note) {
const data = `${recipient}|${amount}|${note}|${Date.now()}`;
const signature = hmacSha256(data, SECRET_KEY);
const exp = Date.now() + 3600000; // 1 hour
return `https://go.yourapp.com/pay?to=${recipient}&amount=${amount}¬e=${encodeURIComponent(note)}&exp=${exp}&sig=${signature}`;
}
The app verifies the signature before showing the payment form:
function verifyPaymentLink(url) {
const params = Object.fromEntries(new URL(url).searchParams);
// Check expiration
if (parseInt(params.exp) < Date.now()) {
throw new Error('Link expired');
}
// Verify signature
const data = `${params.to}|${params.amount}|${params.note}|${params.exp}`;
const expectedSig = hmacSha256(data, SECRET_KEY);
if (params.sig !== expectedSig) {
throw new Error('Invalid signature');
}
return params;
}
3. Replay Attacks
An attacker captures a valid payment deep link and uses it again:
User sends $25 to Jane via deep link.
Attacker captures the link.
Attacker sends the same link to the user again.
User taps it and accidentally sends $25 to Jane again.
Mitigation:
- Include a nonce (one-time token) in the URL
- Mark nonces as used after the first successful processing
- Set short expiration times (1-4 hours)
function createOneTimePaymentLink(recipient, amount) {
const nonce = generateNonce(); // Random, unique
await storeNonce(nonce, { recipient, amount, expiresAt: Date.now() + 3600000 });
return `https://go.yourapp.com/pay?nonce=${nonce}`;
}
function processPaymentLink(nonce) {
const data = await consumeNonce(nonce); // Returns data and marks as used
if (data === null) {
throw new Error('Link already used or expired');
}
return data;
}
4. Man-in-the-Middle
An attacker intercepts the deep link in transit (e.g., via a compromised Wi-Fi network).
Mitigation: HTTPS only. Universal Links and App Links already require HTTPS. Ensure your AASA and assetlinks.json files are also served over HTTPS without redirects.
5. Phishing via Deep Links
An attacker sends a deep link that looks like it goes to your app but actually leads to a phishing page:
Fake: https://go.yourapp-secure.com/login (looks similar, different domain)
Real: https://go.yourapp.com/login
Mitigation:
- Universal Links only open the app for domains you've registered; phishing domains would open in the browser, not the app
- Educate users to look for the app opening directly (not a browser page)
- Never ask users to enter credentials on a page opened from a deep link; the app should already have their session
Authentication Requirements
Always Authenticate for Financial Actions
const REQUIRES_AUTH = [
'/pay/',
'/transfer/',
'/send/',
'/withdraw/',
'/settings/security',
'/account/close',
];
function handleDeepLink(url) {
const path = new URL(url).pathname;
if (REQUIRES_AUTH.some(prefix => path.startsWith(prefix))) {
if (user.isAuthenticated === false) {
pendingDeepLink.save(url);
navigation.navigate('BiometricAuth');
return;
}
}
processDeepLink(url);
}
Session Validation
Even if the user is "logged in," verify the session is still valid before processing a financial deep link:
async function processFinancialDeepLink(url) {
// Validate session with server
const sessionValid = await validateSession();
if (sessionValid === false) {
// Session expired, re-authenticate
pendingDeepLink.save(url);
navigation.navigate('Login');
return;
}
// Additional biometric check for high-value actions
const amount = parseAmount(url);
if (amount > HIGH_VALUE_THRESHOLD) {
const bioAuth = await requestBiometric();
if (bioAuth === false) return;
}
handleRoute(url);
}
Time-Bound Sessions
For deep links to financial screens, require a fresh authentication if the last authentication was more than a set period ago:
const MAX_AUTH_AGE = 5 * 60 * 1000; // 5 minutes
function requireFreshAuth() {
const lastAuth = authStore.lastAuthTimestamp;
return (Date.now() - lastAuth) > MAX_AUTH_AGE;
}
Input Validation
Validate Every Parameter
Never trust data from a URL. Validate all parameters server-side before processing:
function validatePaymentParams(params) {
const errors = [];
// Amount: must be a positive number within limits
const amount = parseFloat(params.amount);
if (isNaN(amount) || amount <= 0) {
errors.push('Invalid amount');
} else if (amount > DAILY_LIMIT) {
errors.push('Amount exceeds daily limit');
}
// Recipient: must exist and be active
if (params.to && isValidIdentifier(params.to) === false) {
errors.push('Invalid recipient');
}
// Note: sanitize for XSS
if (params.note) {
params.note = sanitizeText(params.note);
}
return { valid: errors.length === 0, errors, params };
}
Prevent Injection
Deep link parameters could contain malicious payloads:
https://go.yourapp.com/search?q=<script>steal(document.cookie)</script>
Sanitize all string parameters before rendering them in the UI:
function sanitizeParam(value) {
return value
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
In React Native and Flutter, the UI frameworks typically handle this automatically (no HTML rendering), but be cautious if you're using WebViews or HTML rendering.
Secure AASA and assetlinks.json
AASA Security
- Serve AASA over HTTPS only
- Use TLS 1.2 or higher
- Don't serve AASA from a CDN that could be compromised; use your own server or your deep linking platform
- Restrict paths in the AASA to only the paths your app handles (don't use a wildcard
*for everything)
assetlinks.json Security
- Include both your upload key and Google Play App Signing key fingerprints
- Rotate assetlinks.json when you rotate signing keys
- Monitor for unauthorized changes to the file
Monitoring and Alerting
What to Monitor
| Event | Alert Threshold |
|---|---|
| Failed payment deep link attempts | 10+ per user per hour |
| Expired link access attempts | 50+ per hour (may indicate replay attack) |
| Invalid signature attempts | Any (indicates tampering) |
| Unknown recipient in deep links | 20+ per hour |
| Deep links from unusual geolocations | Any (for high-value transactions) |
Audit Logging
Log all deep link processing for financial flows:
function auditDeepLink(userId, url, result) {
auditLog.write({
timestamp: new Date().toISOString(),
userId: userId,
action: 'deep_link_processed',
path: new URL(url).pathname,
result: result, // 'success', 'auth_required', 'expired', 'invalid_sig', 'error'
ipAddress: getClientIP(),
deviceFingerprint: getDeviceFingerprint(),
});
}
Security Checklist
- Universal Links / App Links only (no custom URL schemes for financial flows)
- No PII or sensitive data in URLs
- Signed URLs for payment and transfer deep links
- One-time nonces for transaction deep links
- Link expiration (1-4 hours maximum)
- Authentication required before financial screens
- Biometric step-up for high-value transactions
- Session freshness check (re-auth if session is older than 5 minutes)
- All parameters validated and sanitized
- Audit logging for all financial deep link processing
- Monitoring and alerting for suspicious patterns
- AASA paths restricted (not wildcard)
For the fintech deep linking overview, see Deep Linking for Fintech and Banking Apps. For deep linking features, see Tolinku deep linking.
Get deep linking tips in your inbox
One email per week. No spam.