DEV Community

Cover image for Mobile App Security Best Practices in 2026
Russel Dsouza
Russel Dsouza

Posted on • Originally published at rapidnative.com

Mobile App Security Best Practices in 2026

  • 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, never AsyncStorage. 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,
});
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 ci in CI, never npm 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,
});
Enter fullscreen mode Exit fullscreen mode

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);
  }
};
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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-console in release builds.
  • jail-monkey for 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.
Enter fullscreen mode Exit fullscreen mode

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 .cursorrules or .github/copilot-instructions.md with your secure defaults.
  • Review AI-generated auth, storage, and network code with the same rigor as a new contributor's PR.
  • Run semgrep and eslint-plugin-security on 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)

Collapse
 
evans_owusu_6801c8d54ae89 profile image
Evans Owusu

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?