Mobile App Security skill for Codex
Treat the app binary as attacker-owned. Codex reviews a mobile app group by group against OWASP MASVS v2, reading both source and the built release artifact with jadx, apktool and plist tooling. It flags tokens in plaintext storage, exported components, ATS and cleartext exceptions, WebView bridges and bundled secrets, maps each finding to a control, and pushes authorization fixes to the server instead of the client.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- mobile-app-security Installs to ~/.agents/skills/mobile-app-security/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install mobile-app-security The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project mobile-app-security Installs to .agents/skills/mobile-app-security/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/mobile-app-security.zipunzip -o mobile-app-security.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) mobile-app-security PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $mobile-app-security, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Reviews iOS and Android apps against OWASP MASVS v2 and the MASTG - insecure local storage, Keychain and Keystore use, cleartext traffic and pinning, exported components, deep links, WebViews, hardcoded secrets and release build settings - using source review plus static tools like MobSF, jadx and apktool. Use when auditing a React Native, Flutter, Swift or Kotlin app before release, when a pentest report cites MASVS, or when asked whether tokens, files or network calls in a mobile app are handled safely.
Use it when
- A mobile app is about to ship and needs a security pass
- You need to know whether tokens, keys or PII are stored safely on device
- A pentest or store review cites MASVS or MASTG controls
- You suspect API secrets are bundled into the app binary
Use something else when
- You are preparing the App Store submission itself: use ios-app-store-release
- The weakness is in the backend API, not the app: use api-security-testing
How to check the result
Rerun manifest, plist and MobSF checks against the rebuilt release artifact, and confirm on an emulator that tokens are absent from app storage and a proxied release build refuses user-installed CAs.
The mistake it prevents
Treating root detection, pinning or obfuscation as the fix for an authorization flaw that only the server can close.
Done well means: every MASVS control group was checked against the code and the built artifact (not only one of them), each finding names the file or manifest line plus the MASVS control it maps to, and the client is never trusted to enforce anything the server must enforce. A mobile app is a binary the attacker owns; review it with that assumption.
Scope and setup
- Confirm what is in scope: source repo, a release build (
.ipa/.aab/.apk), backend APIs. Dynamic testing (Frida, objection, proxying a device) requires written authorization from the app owner and a test device or emulator, never a user’s phone. - Read
AGENTS.md, then identify the stack: native (Swift/Kotlin), React Native, Flutter, Capacitor. Cross-platform apps have two layers to review: the JS/Dart code and the native config (Info.plist,AndroidManifest.xml, entitlements, Gradle). - Pick the MAS testing profile with the owner: L1 (baseline, every app), L2 (apps handling sensitive data such as health, finance, identity), R (resilience against reverse engineering, only when the business model needs it).
Tools (installs need network; ask for approval in the Codex sandbox):
jadx -d out/jadx app-release.apk # decompile to Java/Kotlin-ish source
apktool d app-release.apk -o out/apktool # decoded manifest and resources
bundletool build-apks --bundle=app.aab --output=app.apks --mode=universal # aab -> apk
apksigner verify --print-certs app-release.apk
unzip -q App.ipa -d out/ipa && plutil -p out/ipa/Payload/*.app/Info.plist
codesign -d --entitlements - out/ipa/Payload/*.app
docker run --rm -p 8000:8000 opensecurity/mobile-security-framework-mobsf # MobSF static scan UI
Review by MASVS group
| Group | Check | Red flag |
|---|---|---|
| STORAGE | Where tokens, PII and caches live | Tokens in UserDefaults, SharedPreferences, AsyncStorage, SQLite or files without protection; sensitive data in logs or screenshots |
| CRYPTO | Keys and algorithms | Hardcoded keys or IVs, ECB, MD5/SHA-1 for security, custom crypto |
| AUTH | Session and local auth | Biometric check that only gates UI (boolean) instead of unlocking a Keychain/Keystore key; long-lived tokens with no server-side revocation |
| NETWORK | Transport | NSAllowsArbitraryLoads, usesCleartextTraffic="true", user CAs trusted in release, disabled hostname verification |
| PLATFORM | IPC, WebViews, deep links | Exported activities/receivers/providers without permissions; addJavascriptInterface on remote content; deep-link params used unvalidated |
| CODE | Build and dependencies | debuggable release, outdated SDKs, vulnerable third-party libraries, no minimum OS policy |
| RESILIENCE | Anti-tampering (profile R only) | Relying on root/jailbreak detection as a security control |
| PRIVACY | Data minimisation | SDKs collecting more than declared in the privacy manifest or Data safety form |
Grep pass over source
rg -n "UserDefaults|NSUserDefaults|kSecAttrAccessibleAlways|SharedPreferences|AsyncStorage|MMKV" src ios android lib
rg -n "allowBackup|usesCleartextTraffic|exported=\"true\"|debuggable|networkSecurityConfig" android
rg -n "NSAllowsArbitraryLoads|NSExceptionDomains|LSApplicationQueriesSchemes" ios
rg -n "setJavaScriptEnabled|addJavascriptInterface|setAllowFileAccess|WKWebView|evaluateJavascript" .
rg -n -i "api[_-]?key|secret|BEGIN (RSA|EC) PRIVATE|firebase.*key" --glob '!**/node_modules/**' .
rg -n "X509TrustManager|checkServerTrusted|ALLOW_ALL_HOSTNAME|HostnameVerifier" android
Platform rules
Android
- Since Android 12 every component with an intent filter must declare
android:exported. Anythingexported="true"must validate its input and, if not meant for other apps, be set tofalseor protected with asignaturepermission. - Use a
network_security_config.xmlthat trusts only system CAs in release; user CAs only inside<debug-overrides>. - Store secrets with keys from the Android Keystore (
setUserAuthenticationRequiredfor biometric-gated keys). The Jetpack Security crypto library (EncryptedSharedPreferences) is deprecated; prefer Keystore-backed keys with DataStore, or Tink. android:allowBackup="true"(default) copies app data into cloud and device backups. SetdataExtractionRules(Android 12+) andfullBackupContentto exclude tokens and databases.FLAG_SECUREon screens that show secrets or one-time codes.
iOS
- Keychain items use
kSecAttrAccessibleWhenUnlockedThisDeviceOnly(orAfterFirstUnlock... ThisDeviceOnlyfor background refresh). NeverkSecAttrAccessibleAlways. - Files with sensitive content use Data Protection
.completeFileProtection. - ATS exceptions must be narrow (
NSExceptionDomainsfor one host with a reason), neverNSAllowsArbitraryLoadsin a release build. - Universal links over custom URL schemes for anything carrying tokens; any app can claim a custom scheme.
- Hide sensitive views when the app moves to background (snapshot in the app switcher).
Cross-platform
- React Native:
AsyncStorageis plaintext. Usereact-native-keychainorexpo-secure-store. Check that release bundles do not ship source maps or.envvalues (anything inEXPO_PUBLIC_*orreact-native-configis in the binary). - Flutter:
shared_preferencesis plaintext; useflutter_secure_storage. Release builds with--obfuscate --split-debug-infoonly if profile R applies; obfuscation is not a security boundary.
Hard rules
- Any key, API secret or signing material in the app is public. If the code needs a secret to call a third-party API, move that call behind your backend.
- Root/jailbreak detection, pinning and obfuscation raise cost; they never replace server-side authorization. Every authorization finding is fixed on the server.
- Certificate pinning needs a backup pin and a rollout plan. Pinning to a leaf that rotates bricks the installed base; pin to the SPKI of an intermediate or your own CA, with a remote kill switch.
- App attestation (Play Integrity API, App Attest) is a risk signal for the backend, not a gate the client checks itself.
- Do not publish, upload to public scanners or share the client’s binary or findings outside the engagement without explicit permission.
Verification
- Rebuild the release variant and rerun the manifest and plist checks against the built artifact, not the source tree; build flavors often override manifests.
- Re-scan with MobSF and compare against the first run.
- For storage fixes, install on an emulator, log in, then inspect the sandbox
(
adb shell run-as <pkg> ls -Ron debuggable test builds; on iOS, the simulator’s~/Library/Developer/CoreSimulator/Devices/.../data/Containers/Data/Application/). Tokens must not appear in plaintext. - For network fixes, proxy a test device through an intercepting proxy with a user CA: a release build must refuse the connection.
Output template
## Mobile security review: Acme Wallet 4.2.0 (iOS 4.2.0 build 311, Android 4.2.0 vc 4200)
Profile: MAS-L2 | Scope: source @ a1b2c3d, release builds, /v2 API | Dynamic tests: emulator only
| ID | MASVS | Severity | Finding | Location | Fix |
|---|---|---|---|---|---|
| M-01 | STORAGE | High | Refresh token in AsyncStorage | src/auth/session.ts:42 | Move to react-native-keychain, WhenUnlockedThisDeviceOnly |
| M-02 | PLATFORM | Medium | Exported DeepLinkActivity passes `url` to WebView unvalidated | AndroidManifest.xml:88, DeepLinkActivity.kt:31 | Allowlist hosts; exported=false for internal routes |
Not tested: RESILIENCE (profile R out of scope); backend authz beyond /v2/wallet. What the zip contains
SKILL.md plus the interface file below. Nothing runs on install.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "Mobile App Security"
short_description: "Treat the app binary as attacker-owned"
default_prompt: "$mobile-app-security Check how our React Native app stores the auth token and whether that is safe" About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.