Short answer: a banking app security checklist should follow the OWASP MASVS v2 control groups: STORAGE, CRYPTO, AUTH, NETWORK, PLATFORM, CODE, RESILIENCE and PRIVACY. Keep secrets in the Keychain or Android Keystore, let the server make every authorization decision, enforce TLS, treat jailbreak and root detection as defense in depth, and verify it all with the OWASP MASTG.
In a banking app a leak is both a regulatory event and a direct financial loss. This checklist covers the client-side controls I check when building or auditing fintech and banking apps, from authentication to incident response, and maps each one to the OWASP Mobile Application Security Verification Standard (MASVS) so it lines up with the standard security reviewers already use.
The server half (token lifecycle, policy enforcement, rate limiting and abuse control) is in the Laravel API security checklist. The two are meant to be read together: a hardened app in front of an API that trusts the client is still an open attack path. For how these controls fit a regulated release process, see mobile architecture audits, and for having them built into a new banking app from the start, see fintech and banking app development.
How does OWASP MASVS map to a banking app?
MASVS v2 dropped the old L1/L2/R verification levels from the standard itself and organizes requirements into eight control groups. The levels live on as MAS testing profiles, which decide how strictly each control is tested; a banking app is what the stricter profiles exist for. The table maps each group to what it means for a banking app, the concrete platform check, and the section that covers it.
| MASVS group | What it means for a banking app | Concrete iOS / Android check | Section |
|---|---|---|---|
| MASVS-STORAGE | Tokens, account data and statements never land in plaintext files, backups, caches or the clipboard. | iOS: Keychain with a ThisDeviceOnly accessibility class; files under NSFileProtectionComplete. Android: Keystore-wrapped keys; allowBackup off or dataExtractionRules excluding sensitive files. | 3 |
| MASVS-CRYPTO | Platform crypto with modern algorithms, and keys that are never hardcoded in the binary. | iOS: CryptoKit AES-GCM, keys in the Secure Enclave or Keychain. Android: Keystore keys, StrongBox where the device has it. | 3 |
| MASVS-AUTH | The server decides who the user is; biometrics unlock a credential instead of returning a yes/no. | iOS: Keychain item with SecAccessControl (.biometryCurrentSet). Android: Keystore key with setUserAuthenticationRequired, used through BiometricPrompt with a CryptoObject. | 1 |
| MASVS-NETWORK | TLS everywhere, no cleartext exceptions, and a deliberate decision on certificate pinning. | iOS: App Transport Security with no NSAllowsArbitraryLoads. Android: Network Security Config with cleartextTrafficPermitted="false", optional pin-set with an expiration. | 2 |
| MASVS-PLATFORM | Balances and account numbers do not leak through the app switcher, screenshots, WebViews or IPC. | iOS: cover the window when the scene resigns active; restrict WKWebView JavaScript. Android: FLAG_SECURE on sensitive activities; exported="false" on components that do not need it. | 1, 5 |
| MASVS-CODE | Supported OS versions are enforced, dependencies are current, and all input from outside the app is validated. | Both: minimum OS policy, forced-update path, SDK inventory, deep link and push payload validation. | 5, 7 |
| MASVS-RESILIENCE | Tampering, hooking and debugging are made expensive, as defense in depth rather than as a primary control. | iOS: App Attest, jailbreak and debugger checks. Android: Play Integrity API, root checks, R8 shrinking and obfuscation. | 4, 5 |
| MASVS-PRIVACY | Collect the minimum personal data, keep it out of logs and analytics, and disclose what third-party SDKs do. | iOS: privacy manifest and os_log privacy redaction. Android: Play Data safety form, R8 rules stripping debug logs. | 6 |
1. Authentication & Session Management
MASVS-AUTH, MASVS-STORAGE, MASVS-PLATFORM
Most account takeovers start with a stolen credential or a lifted token, so these controls limit how long any single secret is useful and where it can be used.
Token Lifecycle
- Short-lived access tokens (around 15 minutes) with refresh tokens in the 7 to 30 day range (with Sanctum that means a short token expiry plus a rotate endpoint, or Passport; see the Laravel checklist): short enough that a token captured from a proxy log is dead before anyone reads it, long enough that refresh happens between sessions rather than mid-transfer
- Rotate the refresh token on every use, and revoke the whole family if a rotated-out token is presented again; that is the signal a token was copied
- Invalidate tokens server-side on logout. Expiry alone leaves a stolen token valid until the clock runs out
- Keep access tokens in memory only, never in UserDefaults or SharedPreferences
- Store refresh tokens in the iOS Keychain with
kSecAttrAccessibleWhenUnlockedThisDeviceOnly, or encrypted with an Android Keystore key - For high-value apps, bind the session to the device: a non-exportable Secure Enclave or Keystore key, registered with the server at enrolment, signs each request, so a copied token cannot be replayed elsewhere
Biometric & MFA
- Treat Face ID, Touch ID and Android biometrics as a local convenience that unlocks a stored credential. The server authenticates the credential; a biometric result alone never grants access
- Make biometrics unlock a secret instead of returning a boolean: a Keychain item protected by
SecAccessControlwith.biometryCurrentSeton iOS, or a Keystore key withsetUserAuthenticationRequired(true)used throughBiometricPromptwith aCryptoObjecton Android. A hookedevaluatePolicycallback then unlocks nothing, and.biometryCurrentSetinvalidates the item if a new fingerprint or face is enrolled - Require a second factor for transfers above a threshold, password changes, and new payees
- Step-up within a session: re-authenticate before sensitive operations even if the session is fresh
- Prefer TOTP or push approval over SMS codes. SIM-swap fraud is the standard way SMS OTP is defeated, and it is cheap
Passkeys Instead of Passwords
- Passkeys (FIDO2/WebAuthn) remove the password from sign-in, so there is nothing to phish, reuse or stuff. The private key never leaves the device or the user’s credential manager, and each passkey is bound to your domain, so a look-alike site cannot use it
- On iOS 16 and later use
ASAuthorizationPlatformPublicKeyCredentialProviderwith an associated domain (webcredentials:). On Android 9 and later use the Credential Manager API. Both talk to the same WebAuthn relying-party endpoints on your server - Synced passkeys (iCloud Keychain, Google Password Manager) survive a lost phone, which matters for account recovery. NIST SP 800-63B-4 accepts synced passkeys at AAL2 when the sync service meets its requirements; for the highest assurance, keep a device-bound key such as the Secure Enclave or Keystore key used for session binding above
- Roll out alongside the existing login, not as a replacement on day one. Offer passkey creation after a successful sign-in, keep step-up checks for transfers and new payees, and make recovery at least as strong as sign-in, since a weak reset flow undoes the passkey
- The passkey, biometric, device-binding and recovery code for all of this is in secure authentication best practices for fintech apps
Session Controls
- Auto-lock after a short inactivity window; 2 to 5 minutes is typical for banking apps. Make it configurable only within that range
- Limit concurrent sessions per user, show the user their active devices, and let them sign one out. On a Laravel backend, laravel-device-tokens gives you one Sanctum token per device with sign-out per device or everywhere
Screenshot and App-Switcher Masking
- iOS takes a snapshot of the screen for the app switcher when the app goes to the background. Cover the window with a privacy view in
sceneWillResignActiveand remove it insceneDidBecomeActive, so balances and account numbers never reach that snapshot - iOS has no public API to block screenshots. Detect screen recording and mirroring with
UIScreen.isCaptured, react touserDidTakeScreenshotNotification, and hide sensitive values while capture is active - On Android, set
WindowManager.LayoutParams.FLAG_SECUREon sensitive activities. It blocks screenshots, screen recording and the recents-screen preview in one step
2. API & Network Security
MASVS-NETWORK
Transport Security
- TLS 1.2 or later only, enforced at the server. ATS already refuses anything older on iOS, so the client side is mostly about not adding exceptions
- App Transport Security with no exceptions.
NSAllowsArbitraryLoadsin a production Info.plist is a finding on its own - On Android, ship a Network Security Config with
cleartextTrafficPermitted="false"and no user-added CAs trusted in release builds (scopedebug-overridesto debug only) - Validate the full certificate chain. A
URLSessionDelegateorTrustManagerthat accepts any certificate to make a staging environment work has a way of surviving into release
Certificate Pinning: Trade-offs
Pinning stops interception by a mis-issued or user-installed certificate. It does not stop an attacker who controls the device, and a badly managed pin locks every user out on the day a certificate renews.
- Pin the public key (SPKI hash) rather than the whole certificate, so renewing a certificate with the same key does not break the app
- Include at least one backup pin for a key that is not yet in use, so rotation does not force an app update
- iOS: use
NSPinnedDomainsin the ATS configuration or validate in aURLSessionDelegatetrust challenge. Android: use a<pin-set>with anexpirationdate in Network Security Config, so an outdated build falls back to normal TLS validation rather than failing closed - Have a remote way to update or relax pins, and a runbook for certificate rotation, before you ship pinning
Request Integrity
- TLS already protects requests in transit. Request signing earns its place as device binding: sign with the non-exportable Secure Enclave or Keystore key from section 1, so a bearer token is useless off the device it was issued to. An HMAC with a per-session secret delivered over the same TLS channel adds little
- With signing in place, include a timestamp and nonce in the signed payload and reject anything outside a few-minute window, so a captured request cannot be replayed
- Send tokens only in the
Authorization: Bearerheader. Query strings end up in access logs, proxies, and crash reports - Expect HTTP 429 with
Retry-Afterand honour it with backoff. A client that retries instantly turns a brief server incident into a longer one - Error responses must not leak stack traces, SQL, or whether an account exists. Field-level validation messages are fine; “no account with that email” is not
3. Data Storage & Encryption
MASVS-STORAGE, MASVS-CRYPTO
What Goes Where on iOS
- Keychain: tokens, passwords, private keys, PINs. Use
kSecAttrAccessibleWhenUnlockedThisDeviceOnlyso items are never migrated to another device through a backup - Core Data / SQLite: financial records, encrypted with SQLCipher or protected with the Data Protection class
NSFileProtectionComplete. Data Protection only makes the file unreadable while the device is locked; data that must stay unreadable on a jailbroken, unlocked device needs application-level encryption too - UserDefaults: non-sensitive UI preferences only. It is a plist on disk and lands in backups
- Cached images and PDFs: disable or clear the
URLCachefor sensitive responses and mark files withNSFileProtectionComplete. Downloaded statements are the classic leak here - Backups: set
isExcludedFromBackupon files that should not leave the device - Clipboard: copy sensitive values with
UIPasteboardoptions.localOnlyand an.expirationDate(60 seconds is enough for an account number), so they do not sync through Universal Clipboard or linger
What Goes Where on Android
- Android Keystore: generate keys inside the Keystore so the key material is non-exportable; request StrongBox with
setIsStrongBoxBacked(true)where the device supports it and fall back when it does not - App-private storage: encrypt tokens and records with a Keystore-held key before writing them. Never plain SharedPreferences or external storage
- Backups: set
android:allowBackup="false", or usedataExtractionRules(Android 12+) to exclude sensitive files from cloud backup and device-to-device transfer - Clipboard: mark copied sensitive values with
ClipDescription.EXTRA_IS_SENSITIVE(Android 13+) so they are not shown in the clipboard preview
Encryption Standards
- AES-256-GCM for symmetric encryption, which gives confidentiality and integrity in one primitive (CryptoKit
AES.GCMon iOS, a Keystore-backedCipheron Android). Never reuse a nonce with the same key - Use the Secure Enclave for private keys when available. It holds P-256 keys only, so wrap symmetric keys with an enclave key rather than storing them inside it
- Never hardcode keys. Generate them in the Keystore or Secure Enclave; derive from a user secret only when the data must survive without the server
- Zero sensitive buffers after use. Swift and Kotlin strings cannot be reliably zeroed and may be copied around memory, so keep secrets in mutable byte arrays you can wipe
4. Device & Runtime Integrity
MASVS-RESILIENCE
Jailbreak and Root Detection: Defense in Depth, Not a Control
On a jailbroken or rooted device the sandbox is gone: Keychain items can be dumped, pinning bypassed with one Frida script, and any method hooked. Every detection check runs on that same device, so each one can be patched out. I treat detection as friction plus a risk signal for the server, never as the thing that keeps an account safe. Layered checks still raise the cost:
- Look for jailbreak artifacts: package manager apps, the
/var/jbprefix used by rootless jailbreaks, and injected libraries listed by_dyld_get_image_name. Hardcoded lists of 2015-era paths are the first thing attackers patch - Attempt to write outside the app sandbox; it should fail on a stock device
- Check whether
fork()succeeds; it is denied to sandboxed apps on stock iOS - On Android, check for
subinaries,test-keysbuilds, Magisk and other root-management packages, and whether SELinux is enforcing - Verify the app’s own code signature and bundle identifier at runtime; repackaged apps are a common distribution path for banking malware
- Combine several independent signals and send them to the server. A single boolean in the app is one patch away from always-false
- On detection, degrade rather than refuse: warn the user, disable transfers and payee changes, and log the event server-side with the device signals
Runtime Attack Detection
- Debugger detection on iOS via
sysctl(theP_TRACEDflag) orptrace(PT_DENY_ATTACH).ptraceis not public API, so it is called throughdlsym, and both checks are trivially patched. A speed bump, not a gate - For Objective-C code paths that matter, confirm at runtime that
method_getImplementationstill points at your own code. Swiftfinalclasses and structs are harder to swizzle in the first place - Detect the simulator and emulators, and never ship production credentials or endpoints in builds that can run there
- Use
App Attest(DeviceCheck framework, iOS 14+). It creates a Secure Enclave key attested by Apple; your server verifies the attestation once and a per-request assertion after that. It is the strongest available signal that a request came from your unmodified app - On Android, use the Play Integrity API and verify its verdict on your server, not in the app. Decide in advance what each verdict level (device, app, account) is allowed to do
5. Code Hardening & Obfuscation
MASVS-CODE, MASVS-RESILIENCE, MASVS-PLATFORM
- Xcode release builds on arm64 already produce position-independent executables with stack protection. Verify nothing in the build settings turned them off (
otool -hvshows thePIEflag), and never ship Swift built with-Ounchecked, which removes overflow and precondition checks - Strip symbols from release builds (
Deployment PostprocessingandStrip Linked Productset to YES) and upload the dSYM to your crash reporter, so stack traces are readable to you and not to whoever downloads the IPA - Obfuscate sensitive strings and class names where the threat model justifies it: iXGuard on iOS, DexGuard on Android. R8 alone shrinks and renames; it is not meaningful obfuscation against a determined reverser
- Remove
NSLog,print, and debug output from production builds. A#if DEBUGwrapper is enough on iOS; section 6 has the Android R8 rule - Restrict JavaScript in
WKWebView(WKWebpagePreferences.allowsContentJavaScript = false) unless the page needs it, and never expose a native bridge (WKScriptMessageHandler,addJavascriptInterface) to content you do not control - Treat deep links, universal and app links, and push payloads as untrusted input: validate every parameter and never let a deep link complete an action by itself. Set
android:exported="false"on components that do not need to be reachable - Enforce a minimum supported OS version and keep a forced-update path, so a build with a known vulnerability can be retired without waiting for users
- Inventory third-party SDKs. Each one ships with your signing identity and permissions; review what it collects, how often it updates, and whether it has a privacy manifest
6. Logging: What NOT to Log
MASVS-STORAGE, MASVS-PRIVACY
Logs are the storage location nobody threat-models. On iOS, NSLog and unredacted os_log output goes to the unified log, readable from any Mac the phone is plugged into and included in sysdiagnose bundles. Server logs are often less protected than the database they describe.
Never Log
- Full account numbers, card numbers, CVV
- Auth tokens, refresh tokens, session IDs
- Passwords (even hashed)
- Full API request/response bodies
- PII: SSN, DOB, full address
- Biometric data or persistent device identifiers
Safe to Log
- Event types (login_attempt, transfer_initiated)
- Masked identifiers (last 4 of account)
- Error codes and HTTP status codes
- Request duration and latency
- Feature flags and A/B experiment IDs
- Random per-launch correlation IDs that are not the authenticated session identifier
Secure Logging in the App
- On iOS, use
Logger/os_logand keep dynamic values at the default.privatelevel so they are redacted outside a debugger. Marking a value.publicis a code review item - On Android, strip
Log.dandLog.vfrom release builds with an R8-assumenosideeffectsrule onandroid.util.Log - Check what crash reporters and analytics SDKs capture. Breadcrumbs, network logging, and session replay can collect request bodies and screen contents by default
Server-Side Audit Logging
- Log every authentication event with timestamp, IP, device identifier, and outcome
- Log high-value transactions with user ID, amount, and a masked counterparty
- Log admin actions and configuration changes with the actor
- Ship logs to a SIEM (Splunk, Datadog, CloudWatch) with retention set by your compliance regime. PCI DSS requires twelve months with three months immediately available; treat that as the floor
- Alert on brute-force patterns, impossible travel, and unusual transaction volume per account, not just per system
7. Incident Response Readiness
MASVS does not cover incident response, but a banking app cannot ship without it. The three kill switches below separate a two-hour incident from a two-week one, because each removes the dependency on users updating the app.
Kill Switches
- Remote feature flags that can disable any feature (transfers, new payees, a vulnerable SDK) without an app update
- A forced-update mechanism driven by a server-controlled minimum build, so a compromised version can be blocked the same day
- Server-side session revocation for one user or every user, tested in staging before you need it
Detection & Response Playbook
- Define P0/P1/P2 severity levels with response SLAs before launch
- Name a single incident owner with authority to pull the kill switches
- Prepare user notification templates for credential-exposure scenarios in advance; writing them during the incident is how they go out late
- Know the regulatory clocks before launch. GDPR gives 72 hours to notify the supervisory authority; the US banking agencies’ Computer-Security Incident Notification rule requires a banking organization to notify its primary federal regulator within 36 hours of determining that a notification incident has occurred (a bank service provider must notify the bank as soon as possible); PCI DSS card-brand rules and US state breach laws each set their own deadlines
- Run a tabletop exercise each quarter against a realistic scenario: a leaked signing key, a malicious SDK update, a credential-stuffing run
How do you test a banking app against MASVS?
MASVS says what to achieve; the OWASP Mobile Application Security Testing Guide (MASTG) says how to verify it, with iOS and Android test cases organized by the same control groups. I use it to structure internal reviews and to scope external penetration tests, so both produce evidence in the same shape.
- Scope the review by MASVS group, and agree which MAS testing profile applies before testing starts
- Run static checks on every release build: entitlements, ATS and Network Security Config, exported components, backup flags, hardcoded secrets
- Run dynamic checks on a test device: inspect what ends up in the Keychain, Keystore, files, logs and the app-switcher snapshot
- Record results per control so the evidence is ready for compliance and audit reviews
Mobile App Penetration Testing Checklist for Fintech Apps
When I scope a pen test on a banking or fintech app, iOS and Android, these are the checks I make sure are in it. Each one maps back to a section above.
- Traffic: route the app through an intercepting proxy, confirm TLS is enforced, and try to bypass certificate pinning
- Storage: pull the app container and check the Keychain or Keystore, databases, caches, logs and screenshots for tokens or personal data
- API authorization: replay and edit requests to read another user’s accounts, change amounts or skip steps; the server must refuse every one
- Session: confirm tokens expire, logout revokes them on the server, and a stolen token cannot be used from a second device
- Runtime: run on a jailbroken or rooted device with Frida, and check that tampering is detected and reported to the server
- Binary: decompile the release build for hardcoded keys, test endpoints, debug menus and verbose logging
- Platform: check deep links, exported Android components, pasteboard use and backups for data that leaks out of the app
Security Audit Checklist Summary
- Short-lived tokens + refresh rotation
- Biometrics unlock a Keychain/Keystore secret
- TLS 1.2+, ATS and Network Security Config
- Certificate pinning with backup pins
- Device-bound request signing
- Keychain with device-only access
- AES-256-GCM encryption at rest
- App-switcher masking and FLAG_SECURE
- Jailbreak/root detection (layered)
- App Attest / Play Integrity
- Release build hardening verified
- Symbols stripped from release
- No PII in any log output
- SIEM with anomaly alerts
- Remote kill switches in place
- Incident response playbook ready
Need a Security Audit for Your Mobile Banking App?
If you want this checklist run against your app, see mobile architecture audits. If you are building the app, or a new module in it, see fintech and banking app development.
Request a Security Audit
View Audit Services
Originally published at puneetpal.dev.

