Install
openclaw skills install @ahmadhussain0335/mobile-securityAudits Android, iOS and Flutter apps against OWASP MASVS v2 and the Mobile Top 10 2024: hardcoded secrets, insecure storage (Keystore, Keychain), TLS and pinning, exported components, deep links, WebViews, biometrics, logging of personal data and supply chain. Reports findings with MASVS IDs, severity and evidence. Use for security reviews, pentest prep, or changes to auth, crypto, storage, network or IPC code.
openclaw skills install @ahmadhussain0335/mobile-securityAudit a mobile app the way a mobile pentester and a compliance reviewer would together: find what an attacker on the network, a malicious app on the device, or someone holding an unlocked or rooted phone can actually exploit, map it to an OWASP MASVS v2 control, and show the smallest fix that closes it.
Not for: general code quality or concurrency review (use mobile-code-review), or CI pipeline setup
(use mobile-ci-cd, though this skill flags missing dependency scanning and SBOM steps).
mobile-eu-compliance.
Info.plist, entitlements, PrivacyInfo.xcprivacy, .xcconfig, lock files. Flutter: both of these.debug-overrides, #if DEBUG and kDebugMode guards.
A debug-only issue is Low at most, unless it can ship.| Severity | Definition | Examples |
|---|---|---|
| Critical | Exploitable remotely or at scale with no special access; compromises accounts, keys or many users' data | Backend admin key in the binary, trust-all TrustManager, cleartext login, MQTT credentials over port 1883 |
| High | Exploitable with realistic preconditions (same network, malicious app installed, brief physical access); exposes sensitive data or bypasses auth | Exported component leaking tokens, unvalidated deep link loading any URL in a WebView with a JS bridge, boolean-only biometric gate on secrets |
| Medium | Needs strong preconditions (root, jailbreak, backup extraction) or weakens defense in depth | Tokens in plaintext app storage, sensitive screens without screenshot protection, allowBackup with tokens |
| Low | Hardening gap with little direct impact | No obfuscation, verbose non-sensitive logs, missing CT awareness, debug-only issue |
Rate by impact and likelihood in this app's threat model. A finding without a realistic attacker is not High because a checklist says so.
Everything in an APK, IPA or Flutter bundle is readable: BuildConfig, strings.xml, Info.plist,
.xcconfig, assets, --dart-define values. Obfuscation does not change that.
// BAD: ends up as a constant in the APK
buildConfigField("String", "PAYMENTS_SECRET", "\"sk_live_...\"")
// GOOD: app gets a short-lived, user-scoped token from your backend after login
val token = authRepository.sessionToken()
// BAD: Info.plist and xcconfig values ship in plain text
let key = Bundle.main.object(forInfoDictionaryKey: "BROKER_PASSWORD") as? String
androidx.security:security-crypto (EncryptedSharedPreferences, EncryptedFile, MasterKey) is
deprecated. Do not add it; plan migration for existing users. Replacement: an Android Keystore key, either
directly with Cipher or via Tink AEAD, with ciphertext stored in DataStore or a file.
// BAD: plaintext token in SharedPreferences or DataStore
prefs.edit { putString("refresh_token", token) }
// GOOD: Tink AEAD, keyset wrapped by an Android Keystore key (setup in references/android.md)
val ciphertext = aead.encrypt(token.encodeToByteArray(), "refresh_token".encodeToByteArray())
dataStore.edit { it[REFRESH_TOKEN] = Base64.encodeToString(ciphertext, Base64.NO_WRAP) }
// BAD
UserDefaults.standard.set(token, forKey: "refreshToken")
// GOOD: Keychain, not migrated to other devices or backups
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: "com.example.auth",
kSecAttrAccount as String: "refreshToken",
kSecValueData as String: Data(token.utf8),
kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly,
]
let status = SecItemAdd(query as CFDictionary, nil)
// BAD
await prefs.setString('refresh_token', token);
// GOOD
const storage = FlutterSecureStorage(
iOptions: IOSOptions(accessibility: KeychainAccessibility.first_unlock_this_device),
);
await storage.write(key: 'refresh_token', value: token);
AfterFirstUnlock.Random for secrets.Logs, backups, screenshots, the app switcher snapshot, clipboard and keyboard caches all leak.
// BAD: Log.d survives in release unless stripped
Log.d("Auth", "login ok token=$token user=$email")
// GOOD: no secrets or personal data in logs; strip debug logs in release with R8 rules
Log.d("Auth", "login ok")
// BAD: explicitly public in the unified log
logger.info("token \(token, privacy: .public)")
// GOOD
logger.info("login ok for user \(userId, privacy: .private(mask: .hash))")
window.setFlags(FLAG_SECURE, FLAG_SECURE) on screens with credentials, OTPs, payment or health data.scenePhase leaves .active.dataExtractionRules and fullBackupContent, or disable
backup. Keystore keys never leave the device, so restored ciphertext cannot be decrypted; handle that path.<!-- BAD -->
<application android:usesCleartextTraffic="true" ... >
<!-- GOOD: res/xml/network_security_config.xml -->
<network-security-config>
<base-config cleartextTrafficPermitted="false" />
</network-security-config>
// BAD: disables certificate and hostname validation for everything
OkHttpClient.Builder()
.sslSocketFactory(trustAllFactory, trustAllManager)
.hostnameVerifier { _, _ -> true }
// GOOD: platform defaults
OkHttpClient.Builder().build()
NSAllowsArbitraryLoads; no .useCredential for a server trust that skipped SecTrustEvaluateWithError.badCertificateCallback returning true. dart:io does not apply ATS or the Android network config.IoT and MQTT: TLS always, including on "internal" networks. Never cleartext brokers.
// BAD
MqttClient("tcp://broker.example.com:1883", clientId, MemoryPersistence())
// GOOD: TLS, per-device or per-user credentials, broker-side topic ACLs
MqttClient("ssl://broker.example.com:8883", clientId, MemoryPersistence())
Default: standard CA validation, which Certificate Transparency enforcement strengthens where the platform applies it. Pin only when the threat model requires it (high-value targets, controlled backends, IoT provisioning endpoints) and the team owns key rotation.
// BAD: one pin, no backup; the app stops connecting when the server key rotates
.add("api.example.com", "sha256/<current-spki>")
// GOOD: SPKI pins for current key and a pre-generated backup key, with monitoring and a release plan
CertificatePinner.Builder()
.add("api.example.com", "sha256/<current-spki>")
.add("api.example.com", "sha256/<backup-spki>")
.build()
<pin-set> needs an expiration and a backup pin; iOS uses NSPinnedDomains. Never pin
third-party endpoints you do not control (analytics, CDNs, identity providers).A success callback alone can be hooked on a rooted or jailbroken device. Bind the secret to authentication.
// BAD: boolean gate
biometricPrompt.authenticate(promptInfo)
// onAuthenticationSucceeded { showVault() }
// GOOD: Keystore key with setUserAuthenticationRequired(true); only an authenticated Cipher decrypts
biometricPrompt.authenticate(promptInfo, BiometricPrompt.CryptoObject(cipher))
// onAuthenticationSucceeded { result -> result.cryptoObject?.cipher?.doFinal(encryptedToken) }
// BAD: boolean gate
if try await LAContext().evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: "Unlock") {
showVault()
}
// GOOD: Keychain item protected by access control; the OS releases it only after biometry
let access = SecAccessControlCreateWithFlags(
nil, kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly, .biometryCurrentSet, nil)
// add with kSecAttrAccessControl: access; read with kSecUseAuthenticationContext off the main thread
BIOMETRIC_STRONG for crypto-bound auth. setInvalidatedByBiometricEnrollment(true) and .biometryCurrentSet
invalidate the key on new enrollment. iOS requires NSFaceIDUsageDescription in Info.plist.ASWebAuthenticationSession) with PKCE, not an
embedded WebView. Passkeys via Credential Manager or AuthenticationServices where supported.isAdmin flag is not access control.// BAD: role decided on the client
if (prefs.getBoolean("is_admin", false)) showAdminPanel()
// GOOD: UI hint only; every admin API call is authorized server-side
if (session.claims.roles.contains("admin")) showAdminPanel()
<!-- BAD: internal screen reachable by any app -->
<activity android:name=".DebugMenuActivity" android:exported="true" />
<!-- GOOD -->
<activity android:name=".DebugMenuActivity" android:exported="false" />
// BAD: deep link parameter goes straight into a privileged action
webView.loadUrl(intent.data?.getQueryParameter("url").orEmpty())
// GOOD: allowlist scheme and host
val target = intent.data?.getQueryParameter("url")?.toUri()
if (target?.scheme == "https" && target.host in trustedHosts) webView.loadUrl(target.toString())
onOpenURL / application(_:continue:) and Flutter routers validate the same way before routing.android:autoVerify="true" plus
assetlinks.json) and Universal Links (Associated Domains) for anything carrying tokens or codes.Intent taken from another app's extras (intent redirection).PendingIntent uses FLAG_IMMUTABLE unless mutability is required, with an explicit target component.exported="false" or signature-level permissions; no raw path joins on openFile.// BAD: JS bridge on any page the WebView navigates to
webView.settings.javaScriptEnabled = true
webView.addJavascriptInterface(TokenBridge(session), "Android")
webView.loadUrl(urlFromServerOrDeepLink)
// GOOD: origin-scoped message listener, navigation restricted to an allowlist
if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER)) {
WebViewCompat.addWebMessageListener(webView, "bridge", setOf("https://app.example.com"), listener)
}
WKScriptMessageHandler checks message.frameInfo.securityOrigin and isMainFrame.allowFileAccess, allowFileAccessFromFileURLs and allowUniversalAccessFromFileURLs off.
Serve local content through WebViewAssetLoader.// BAD: dynamic version, unreproducible and unscannable
implementation("com.example:sdk:+")
// GOOD
implementation(libs.example.sdk)
Package.resolved, pubspec.lock, Podfile.lock); scan them in CI (OSV-Scanner,
OWASP Dependency-Check, Dependabot or Renovate) and fail on Critical.R8, Swift symbol stripping and Flutter --obfuscate slow reverse engineering. None is a security boundary.
// BAD: local check is the only control, patched out in minutes
if (RootChecker.isRooted()) finish()
// GOOD: attestation token sent with the sensitive request; backend verifies and decides
val token = integrityTokenProvider.request(
StandardIntegrityTokenRequest.builder().setRequestHash(requestHash).build()).await()
api.transfer(request, integrityToken = token.token())
DCAppAttestService) with server-side attestation and assertion verification;
DeviceCheck for per-device bits.isMinifyEnabled = true, android:debuggable not set, no get-task-allow in distribution.// BAD: telemetry carries direct identifiers it does not need
Telemetry(mac = device.macAddress, serial = device.serial, email = user.email, rssi = rssi)
// GOOD: only what the feature needs, pseudonymous ID rotated per installation
Telemetry(installId = installId, firmware = device.firmwareVersion, rssi = rssi)
neverForLocation for BLE scans
on Android 12+, and the photo picker over broad media access.PrivacyInfo.xcprivacy declares collected data and required-reason APIs, consistent with the
App Store privacy labels. Android: the Play Data safety form matches what the code does.EncryptedSharedPreferences for new code, or CRC as protection against an attacker.Info.plist, entitlements, network config and build types reviewedBuildConfig, Info.plist, --dart-define or git historysecurity-crypto usageFLAG_SECURE or snapshot cover on sensitive screensbadCertificateCallbackCryptoObject or Keychain access control## Security audit: <app / module / MR>
Scope: <files, modules, commit>. Platforms: <Android | iOS | Flutter>. Build reviewed: <release variant>.
Threat model: <assets, attackers considered>.
### Findings
1. [Critical] MASVS-NETWORK-1 / M5 -- Trust-all TrustManager in production client
Evidence: app/src/main/java/com/example/net/HttpModule.kt:42
Impact: any network attacker can read and modify all API traffic, including login credentials.
Fix:
```kotlin
OkHttpClient.Builder().build() // remove custom sslSocketFactory and hostnameVerifier
```
2. [High] MASVS-AUTH-2 / M3 -- Biometric unlock is a boolean gate
Evidence: ios/App/Vault/VaultUnlock.swift:88
Impact: ...
Fix: ...
### Needs dynamic verification
- <items that static review could not confirm, for example "pinning behaviour on proxy interception">
### Summary
| MASVS category | Critical | High | Medium | Low | Notes |
|---|---|---|---|---|---|
| STORAGE | 0 | 0 | 1 | 0 | |
| CRYPTO | 0 | 0 | 0 | 0 | |
| AUTH | 0 | 1 | 0 | 0 | |
| NETWORK | 1 | 0 | 0 | 0 | |
| PLATFORM | 0 | 0 | 0 | 0 | |
| CODE | 0 | 0 | 0 | 1 | |
| RESILIENCE | 0 | 0 | 0 | 0 | |
| PRIVACY | 0 | 0 | 1 | 0 | |
### Not checked
- Backend authorization and API security
- Runtime behaviour (proxy interception, Frida, rooted / jailbroken device testing)
- Compiled binary (only source reviewed)
- <modules, flavors or SDKs out of scope>
### Compliance notes (if EU market or IoT)
- <findings relevant to the CRA, EN 18031 or GDPR; map them with `mobile-eu-compliance`>
Rules for the report:
mobile-eu-compliance -- maps these findings to CRA, RED / EN 18031 and GDPR requirementsmobile-code-review -- general correctness, concurrency and lifecycle reviewmobile-ci-cd -- wiring dependency scanning, SBOM generation and signing into the pipelinemobile-kotlin, mobile-java, mobile-swift, mobile-flutter -- platform idioms behind the fixesmobile-merge-request -- describing security fixes and permission or entitlement changes in the MRmobile-ble -- pairing, command authentication and firmware update on the device linkmobile-release -- privacy manifests, Data safety and store privacy declarations