Back to KB
Difficulty
Intermediate
Read Time
32 min

OWASP Mobile Top 10 for React Native Fintech Apps: A Practical Implementation Checklist

By Codcompass Team··32 min read

TL;DR

The OWASP Mobile Top 10 isn't abstract theory — it's the exact list pen testers use to fail your app. Here's the cheat sheet: M1 — stop storing tokens in AsyncStorage, use Keychain/Keystore. M2 — audit your node_modules before it audits you. M3 — biometric gates belong on every sensitive screen, not just login. M4 — parameterize your SQLite queries and validate deep links. M5 — SSL pin your public keys, not your certs, and always have a backup pin. M6 — your crash reporter is exfiltrating PII right now. M7 — enable Hermes, strip source maps, turn off debug mode. M8 — detect jailbreak/root but let your developers bypass it. M9 — encrypt local data with MMKV + Keychain-stored keys. M10Math.random() is not random; use expo-crypto.


Why This Matters Now

The OWASP Mobile Top 10 was updated in 2024 with a significant restructuring. "Insecure Data Storage" and "Insecure Communication" are still there, but the new list adds "Inadequate Supply Chain Security" (M2) — which feels like it was written specifically for the npm ecosystem — and "Insufficient Binary Protections" (M7), which targets exactly the kind of JavaScript-bundle-in-a-native-shell architecture that React Native uses.

If you're building a fintech app in React Native, you're a target. RN apps ship a JavaScript bundle that can be extracted, decompiled, and analyzed in minutes. Unlike Swift or Kotlin binaries, there's no compilation step that obfuscates your logic by default. An attacker with a rooted Android device can pull your APK, unzip it, and read your business logic in index.android.bundle — including any hardcoded API keys, endpoint URLs, or validation logic you thought was "server-side."

What does a failed security audit actually cost? I've seen three outcomes: App Store rejection during review (Apple has gotten aggressive about checking for jailbreak detection and SSL pinning in fintech apps), a pen test report with 15+ critical findings that delays launch by 2-3 months, or worse — a compliance failure that means you can't process payments at all. PCI DSS, SOC 2, and regional banking regulations all reference OWASP controls.

And the breaches are real. In 2018, British Airways lost data on 400,000 customers due to poor authentication and unsecured third-party scripts — the ICO fined them £20 million. In 2021, ParkMobile exposed 21 million users' data through a vulnerability in a third-party component. These aren't theoretical risks — they're financial and regulatory consequences that hit real companies.

The gap I keep seeing: everyone knows the names of these vulnerabilities. Blog posts explain what "insecure data storage" means. But almost nobody shows the actual TypeScript code. This article is the code.

The companion repository has every file referenced below: github.com/FastheDeveloper/owasp-rn-fintech

Every section includes screenshots from the interactive demo app so you can see these controls in action — not just read about them.


SCREENSHOT: Home screen showing all 8 security demo cards


Project Setup

npx create-expo-app@latest owasp-rn-fintech --template blank-typescript
cd owasp-rn-fintech

# Secure storage & crypto
npx expo install expo-secure-store@15.x expo-crypto@15.x

# Biometrics
npx expo install expo-local-authentication@17.x

# SSL pinning
npm install react-native-ssl-public-key-pinning@1.2.x

# Encrypted local storage
npm install react-native-mmkv@4.x

# Local database
npx expo install expo-sqlite@16.x

# Jailbreak detection
npm install jail-monkey@3.x

Enter fullscreen mode Exit fullscreen mode

Warning: react-native-ssl-public-key-pinning, react-native-mmkv, and jail-monkey require native modules. They won't work in Expo Go — you'll need a development build (npx expo prebuild + npx expo run:ios).


M1: Improper Credential Usage

What it means in React Native: Hardcoded API keys in your source code. Auth tokens stored in AsyncStorage (plaintext JSON files on disk). Secrets committed to your repo via .env files that aren't gitignored. Refresh tokens that never expire.

Real-World Incident: In 2016, Uber's breach exposed 57 million users' and drivers' personal data — names, emails, phone numbers, and 600,000 driver's license numbers. The root cause: Uber engineers had hardcoded AWS credentials in a private GitHub repository. Attackers used those credentials to access an S3 bucket containing the user database. Uber then paid the attackers $100,000 through their bug bounty program to delete the data and keep quiet — and didn't disclose the breach for over a year. Hardcoded credentials in source code is the #1 credential misuse pattern, and it applies equally to mobile apps: API keys in your React Native bundle, tokens in .env files committed to git, or secrets baked into your build config.

I audited a production fintech app recently and found this exact pattern:

// ❌ WRONG — This is what most RN apps do
// store/store.ts (actual production code)

import AsyncStorage from "@react-native-async-storage/async-storage";
import { persistReducer, persistStore } from "redux-persist";

const persistConfig = {
  key: "root",
  storage: AsyncStorage, // ← PLAINTEXT on disk
  whitelist: ["persisted", "auth"], // ← auth tokens persisted in plaintext
};

Enter fullscreen mode Exit fullscreen mode

On a rooted Android device, that data lives at /data/data/com.yourapp/files/RCTAsyncLocalStorage/manifest.json. Every token, every piece of user state — plaintext. Even on iOS, if a user backs up their device unencrypted, those tokens are in the backup.

The fix has two parts: store credentials in Keychain/Keystore (hardware-encrypted), and implement session expiry so stolen tokens have a shelf life.

// src/security/secure-storage.ts

import * as SecureStore from "expo-secure-store";

const SECURE_STORE_OPTIONS: SecureStore.SecureStoreOptions = {
  keychainAccessible: SecureStore.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
};

export const CredentialStore = {
  async setToken(key: string, value: string): Promise<void> {
    await SecureStore.setItemAsync(key, value, SECURE_STORE_OPTIONS);
  },

  async getToken(key: string): Promise<string | null> {
    return SecureStore.getItemAsync(key, SECURE_STORE_OPTIONS);
  },

  async deleteToken(key: string): Promise<void> {
    await SecureStore.deleteItemAsync(key);
  },

  async clearAll(keys: string[]): Promise<void> {
    await Promise.all(keys.map((key) => SecureStore.deleteItemAsync(key)));
  },
};

export const CREDENTIAL_KEYS = {
  ACCESS_TOKEN: "fintech_access_token",
  REFRESH_TOKEN: "fintech_refresh_token",
  DEVICE_ID: "fintech_device_id",
  ENCRYPTION_KEY: "fintech_encryption_key",
  BIOMETRIC_ENROLLED: "fintech_biometric_enrolled",
} as const;

Enter fullscreen mode Exit fullscreen mode

For session management, the token in Redux (in-memory) is fine for the session lifetime. But you need active timeout enforcement:

// src/security/session-manager.ts

import { AppState, AppStateStatus } from "react-native";
import { CredentialStore, CREDENTIAL_KEYS } from "./secure-storage";

export interface SessionConfig {
  inactivityTimeoutMs: number; // 5 minutes for banking apps
  backgroundTimeoutMs: number; // 2 minutes in background
  heartbeatIntervalMs: number; // Check every 30 seconds
  onSessionExpired: () => void;
  onTokenRefreshNeeded: () => Promise<boolean>;
}

class SessionManager {
  private lastActivityTimestamp: number = Date.now();
  private backgroundTimestamp: number | null = null;
  private heartbeatTimer: ReturnType<typeof setInterval> | null = null;
  private appStateSubscription: any = null;

  start(tokenExpiryMs?: number): void {
    this.lastActivityTimestamp = Date.now();

    // Monitor app state changes (foreground/background)
    this.appStateSubscription = AppState.addEventListener(
      "change",
      this.handleAppStateChange,
    );

    // Periodic heartbeat to check session validity
    this.heartbeatTimer = setInterval(
      () => this.checkSession(),
      this.config.heartbeatIntervalMs,
    );
  }

  recordActivity(): void {
    this.lastActivityTimestamp = Date.now();
  }

  private handleAppStateChange = (nextState: AppStateStatus): void => {
    if (nextState === "background" || nextState === "inactive") {
      this.backgroundTimestamp = Date.now();
    } else if (nextState === "active") {
      if (this.backgroundTimestamp) {
        const backgroundDuration = Date.now() - this.backgroundTimestamp;
        if (backgroundDuration > this.config.backgroundTimeoutMs) {
          this.expireSession("background_timeout");
          return;
        }
      }
      this.checkSession();
    }
  };

  private async expireSession(reason: string): Promise<void> {
    this.stop();
    await CredentialStore.clearAll([
      CREDENTIAL_KEYS.ACCESS_TOKEN,
      CREDENTIAL_KEYS.REFRESH_TOKEN,
    ]);
    this.config.onSessionExpired();
  }
}

Enter fullscreen mode Exit fullscreen mode

Production gotcha: The fintech app I audited stored LAST_ACTIVE_KEY in AsyncStorage for session tracking. That means a sophisticated attacker could modify the timestamp to prevent session expiry. Store session timestamps in memory only — if the app restarts, the session should be treated as expired anyway.

In the Demo App

The Secure Storage screen shows the difference visually. On the left: AsyncStorage storing your token as plaintext JSON. On the right: SecureStore encrypting it with AES-256-GCM in the Secure Enclave. The live demo lets you store, retrieve, and delete a secret — proving the round-trip works and the Keychain entry is fully wiped on delete.

SCREENSHOT: Side-by-side comparison — AsyncStorage (red) vs SecureStore (green)

Checklist

  • [ ] Auth tokens stored in expo-secure-store or react-native-keychain, never AsyncStorage
  • [ ] Session timeout enforced (5 min inactivity, 2 min background for banking)
  • [ ] Tokens cleared on logout, session expiry, and excessive failed auth attempts
  • [ ] No API keys, secrets, or tokens hardcoded in source — use server-side config

M2: Inadequate Supply Chain Security

The RN-specific risk: Your node_modules folder contains hundreds of packages, each maintained by different people with different security practices. A single compromised dependency can exfiltrate user data. This isn't theoretical — event-stream, ua-parser-js, and colors all had real supply chain attacks.

Real-World Incident: In 2021, ParkMobile's breach exposed 21 million users' personal data — names, emails, phone numbers, license plates, and hashed passwords — all because of a vulnerability in a third-party integration. The app itself wasn't directly compromised; a dependency was. In the npm ecosystem, this risk is amplified: your package-lock.json contains hundreds of transitive dependencies you never explicitly chose, each one a potential attack surface.

The event-stream incident (2018) is the textbook example. A user named right9ctrl social-engineered their way into maintainer access of the popular event-stream package (used by 3,931 other packages including @vue/cli-ui, vscode, and nodemon). After a series of innocent commits to build trust, they added a new dependency called flatmap-stream — which contained an encrypted malicious payload hidden in its minified source code. The payload was surgically targeted: it would only decrypt and execute when built as part of Copay, a Bitcoin wallet app, using the app's own npm_package_description as the decryption key. Once active, it harvested Bitcoin wallet private keys and balances above 100 BTC. The attack was downloaded 8 million times and went undetected for over a month — only discovered because of an unrelated OpenSSL deprecation warning in nodemon.

React Native adds extra risk because many packages include native modules (Objective-C, Java/Kotlin) that most JavaScript developers never audit. A native module has full device access — it can read the filesystem, make network calls, and access hardware without any JavaScript-visible API surface.

// package.json — lock your dependencies
{
  "dependencies": {
    "expo-secure-store": "15.0.8",
    "react-native-mmkv": "4.3.1",
    "react-native-ssl-public-key-pinning": "1.2.6"
  }
}

Enter fullscreen mode Exit fullscreen mode

Use exact versions (not ^ or ~) for security-critical packages. Add these to your CI pipeline:

# CI audit command
npm audit --audit-level=high
npx better-npm-audit audit --level high

# Check for known vulnerabilities in native dependencies
npx expo-doctor

# Verify package provenance (npm v9.5+)
npm audit signatures

Enter fullscreen mode Exit fullscreen mode

What to watch for:

  • Packages with no updates in 12+ months (abandonware)
  • Packages with a single maintainer and recent ownership transfer
  • Native modules that request permissions your app doesn't need
  • Transitive dependencies pulling in unexpected native code

Checklist

  • [ ] npm audit runs in CI and blocks on high/critical vulnerabilities
  • [ ] Security-critical packages use exact versions, not ranges
  • [ ] Review native module permissions before adding new dependencies
  • [ ] Package-lock.json is committed and reviewed in PRs

M3: Insecure Authentication and Authorization

The real issue in RN fintech: Most apps only require biometric auth at login. But a pen tester will check whether you can access the payment screen, view account balances, or initiate transfers without re-authenticating. If you authenticate once at login and then trust the session for everything, a stolen unlocked device = full access.

Real-World Incident: In 2018, Facebook's photo API bug exposed 6.8 millio

🎉 Mid-Year Sale — Unlock Full Article

Base plan from just $4.99/mo or $49/yr

Sign in to read the full article and unlock all 635+ tutorials.

Sign In / Register — Start Free Trial

7-day free trial · Cancel anytime · 30-day money-back