-
Most mobile breaches aren't sophisticated. They're a hardcoded API key, a forgotten debug flag, or a token in plaintext
AsyncStorage. - The OWASP Mobile Top 10 (2024) is your checklist — work it every release.
-
Tokens live in the OS keychain via
expo-secure-store, neverAsyncStorage. Period. - Certificate pinning for sensitive endpoints, pinned to the SPKI hash, with a backup pin and a rotation plan.
- AI-generated code is untrusted input — review auth, storage, and network code with the same rigor as a new contributor's PR.
-
CI is where security lives:
semgrep+eslint-plugin-security+npm audit+ MobSF on every release artifact.
Mobile attacks are up. Regulators are watching. AI is writing more of your code than ever — and the patterns it reproduces aren't always the secure ones. Here's the practical checklist I run through for every React Native / Expo app, organized around the OWASP Mobile Top 10 (2024).
This is the working version of a longer guide — focused on what to actually change in your codebase this week.
1. Credentials: nothing sensitive in the bundle, nothing sensitive in AsyncStorage
// ❌ Don't
await AsyncStorage.setItem('access_token', token);
// ✅ Do
import * as SecureStore from 'expo-secure-store';
await SecureStore.setItemAsync('access_token', token, {
keychainAccessible: SecureStore.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
});
Anything in the bundle can be extracted with apktool in minutes. Anything in AsyncStorage is plaintext on disk. Tokens go in the OS keychain via expo-secure-store or react-native-keychain. Period.
Refresh tokens rotate on every use. Access tokens live 15 minutes. The backend is the trust boundary, not the client.
2. Supply chain: assume your dependencies are hostile
# In CI, on every PR
npm ci
npm audit --audit-level=high
A clean package.json doesn't mean a clean app. Post-install scripts run with your dev-machine privileges. Native modules run with full app privileges.
- Lockfiles in source control.
npm ciin CI, nevernpm install. - Add Socket or Snyk for behavioral analysis (what npm audit misses).
- Audit native modules personally if they touch storage, networking, or credentials.
3. Auth: PKCE, short JWTs, server-side authorization
import * as AuthSession from 'expo-auth-session';
// PKCE is the default in expo-auth-session — don't disable it.
const request = new AuthSession.AuthRequest({
clientId,
scopes: ['openid', 'profile'],
usePKCE: true,
redirectUri,
});
OAuth 2.0 with PKCE for third-party identity. JWTs with 15-minute access tokens and rotated refresh tokens. Every endpoint validates the caller server-side. Hiding UI is not authorization.
Add MFA via expo-local-authentication for anything touching payments, identity, or health data.
4. Input validation: deeplinks are user input
// Treat the URL params as hostile
const handleDeepLink = (url: string) => {
const parsed = new URL(url);
const action = parsed.searchParams.get('action');
if (action && /^[a-z_]{1,32}$/.test(action) && KNOWN_ACTIONS.has(action)) {
routeTo(action);
}
};
Deeplinks, push payloads, clipboard, QR codes, WebView messages — all untrusted. Validate type, length, format. Parameterized queries for local SQLite. originWhitelist on every WebView.
5. TLS 1.3 + certificate pinning
<!-- android/app/src/main/res/xml/network_security_config.xml -->
<network-security-config>
<domain-config cleartextTrafficPermitted="false">
<domain includeSubdomains="true">api.yourapp.com</domain>
<pin-set>
<pin digest="SHA-256">{base64-spki-hash}</pin>
<pin digest="SHA-256">{backup-spki-hash}</pin>
</pin-set>
</domain-config>
</network-security-config>
Pin to the SPKI hash, not the leaf cert. Ship a backup pin. Have a rotation plan. Reject TLS 1.0/1.1 server-side.
In Expo, usesCleartextTraffic: false. Verify no allowsArbitraryLoads snuck into production.
6. Privacy controls aren't optional
Maintain a data inventory. Apply data minimization. Request permissions just-in-time with context. Build account-delete-and-export flows that actually delete and export. Audit analytics/ad SDKs quarterly — they change practices on their schedule.
7. Binary hardening: Hermes, R8/ProGuard, App Attest
- Ship Hermes bytecode — much harder to reverse than plain JS.
- R8 with shrinking and obfuscation on Android.
-
babel-plugin-transform-remove-consolein release builds. -
jail-monkeyfor rooted/jailbroken detection (signal, not block). - Apple's App Attest + Google's Play Integrity API to verify the app calling your backend is the one you shipped.
8. Configuration hygiene
-
__DEV__guards on every debug code path. -
android:exported="true"only when truly needed. - URL schemes and Universal Links audited as entry points.
- CI check that fails the build if known test-account strings hit the binary.
9. Encrypted storage, intentional sensitivity tiers
| Sensitivity | Storage |
|---|---|
| Credentials, keys | iOS Keychain / Android Keystore via expo-secure-store
|
| Structured PII | SQLCipher or encrypted Realm |
| Non-sensitive | Regular filesystem or AsyncStorage
|
Disable backup for sensitive paths. Mask app-switcher screenshots on sensitive screens via expo-screen-capture.
10. Modern crypto, vetted libraries
import { randomBytes } from 'react-native-quick-crypto';
// AES-256-GCM. Never CBC without auth. Never ECB. Ever.
Argon2id for password hashing. HKDF for key derivation. SHA-1 and MD5 are dead. Start tracking your post-quantum migration — NIST's ML-KEM and ML-DSA are finalized.
11. Treat AI-generated code as untrusted input
This is the one most security frameworks haven't caught up to. LLMs reproduce the most common pattern in their training data — often the most common flawed pattern.
- Maintain a
.cursorrulesor.github/copilot-instructions.mdwith your secure defaults. - Review AI-generated auth, storage, and network code with the same rigor as a new contributor's PR.
- Run
semgrepandeslint-plugin-securityon AI output before merge. - Pen-test AI-generated payment and auth flows specifically.
12. CI is where security lives
Every PR:
-
semgrep,eslint-plugin-security, Android Lint -
npm audit+ Socket/Snyk - MobSF on release-build artifacts
Annually: pen test. Quarterly: SDK audit. Always: an incident response plan that includes key rotation, token revocation, and an OTA push.
Most mobile breaches aren't sophisticated. They're a hardcoded key, a forgotten debug flag, a plaintext token. The OWASP Top 10 is your checklist — work it every release.
For the longer breakdown — including the AI-safe-code generation patterns and the full OWASP-mapped CI workflow — see the RapidNative blog.
What's the security gotcha you've shipped to production and then quietly patched? Drop it in the comments — I'm collecting the failure modes that don't make it into the OWASP examples for a follow-up.
Top comments (1)
Really timely post. Security in apps that handle personal
or sensitive data is something I've been thinking about a
lot while building Yhuu (yhuu.life) — an anonymous Q&A app
where people ask honest questions in relationships.
The encryption layer was one of the trickiest parts. We're
storing session records that people really don't want
exposed, so getting the data handling right was non-negotiable.
One thing I'd add to your list: be extra careful with
anonymous session tokens. If those get leaked, the whole
anonymity promise breaks down — which for us would be a
trust-ending bug.
What's your take on token storage — AsyncStorage vs
secure enclave solutions?