Insecure Key Storage in Mobile and Single-Page Applications
The real problem is where you store keys, not how you encrypt them.

Insecure key storage in mobile and single-page applications has more to do with where the key is kept than with cryptography. Strong encryption cannot protect a secret shipped inside a readable binary, JavaScript bundle, or backup. The fix is to put secrets behind storage boundaries that block extraction, then test the application that actually ships to see whether those boundaries hold.
Why insecure key storage is a placement problem, not a cryptography problem
Reading the key lets an attacker defeat the protection of a strong cipher. If a debugger, decompiler, or backup tool can recover the secret, the attacker can bypass the need to break the encryption altogether. The failure starts with where the application puts the key.
Mobile binaries and SPA bundles reach that failure by different routes. Both ship code to devices or browsers outside the developer’s control, where embedded values can be inspected. An attacker on mobile might decompile an application, while someone using a browser can search the JavaScript it downloads. The tools differ, but the misplaced trust is the same. A storage choice made early in development becomes part of the shipped product, even if nobody revisits whether that location can keep a secret.
How mobile binaries expose secrets to anyone with a decompiler
A string constant in Java, Kotlin, Swift, or Objective-C can survive compilation as readable text. A decompiler can recover that value from the distributed binary without access to the source repository. The application itself supplies the material for inspection. A key that looked temporary in a development file becomes available to whoever obtains the shipped artifact.
Android’s SharedPreferences exposes a different route to the same result. It stores data as plaintext XML by default at /data/data/{package name}/shared_prefs/. Where supported, MODE_WORLD_READABLE exposes the file to other apps. With ADB access that permits extraction, an attacker can dump the file using adb or file explorer tools.
Backups can provide that access without rooting the device. Where Android’s backup mechanism permits it, enabling android:allowBackup="true" can expose application data, including SharedPreferences and SQLite databases, through an ADB backup. When someone extracts an unencrypted local database, they can read its data too. Debug output adds another way in because sensitive values written to logcat are kept as log data without the protection of credentials. Some Android versions allow other apps to read that output without root.
On iOS, NSUserDefaults stores values in an unencrypted plist under the application’s /Library/Preferences/ directory. A backup of the device can reveal those values if it is unencrypted. NSUserDefaults does not provide the protection of Keychain services. The secret is either readable in the artifact or becomes readable when the surrounding file is extracted.
How SPA bundles expose secrets to anyone with a browser
An SPA ships its entire frontend codebase to untrusted clients. The browser needs that code to run the application, so embedded secrets can reach visitors before they log in. Webpack, Vite, and Rollup turn application code, and sometimes configuration, into downloadable JavaScript. A developer may regard a value as build configuration while the deployed product delivers it as text.
Getting secrets out this way takes less equipment than mobile reverse engineering. Browser developer tools, a curl request to the bundle URL, or a string search through the output can expose embedded credentials. No physical device or root access is needed. CISA’s advisory describes CVE-2025-64308 in Brightpick Mission Control as hardcoded client-side JavaScript credentials that unauthenticated adjacent-network attackers could extract. Its CVSS 3.1 score is 6.5, and the adjacent-network condition is important.
CVE-2025-68429 concerned environment variables from .env files being unexpectedly bundled into build output, potentially exposing secrets to anyone with access to the deployed Storybook. Configuration was not a safe boundary in that case. Beyond credentials, client-side routing can reveal backend endpoints and API Gateway configurations. Reading those definitions can also help an attacker look for authorization gaps and insecure direct object references that do not show up in the visible interface.
Why localStorage is the SPA equivalent of SharedPreferences — and equally dangerous
Moving a token out of source code and into localStorage changes when it arrives while leaving the same scripts able to read it. There is no first-party-only permission boundary around the value. OWASP’s Session Management Cheat Sheet warns against storing tokens there because scripts that run on the page can read them. That includes code that gets in through an unsanitized render, a vulnerable npm package, or a third-party ad tag.
The storage choice therefore amplifies a separate scripting flaw. CWE-79, cross-site scripting, intersects with CWE-522, insufficiently protected credentials, when an injected script reads and exports a token. The attacker does not need to guess a password. Replaying the stolen token need not produce a failed-login alert or a fresh MFA prompt. The credential can leave the browser and become usable elsewhere, rather than remaining confined to the page where the script ran.
HttpOnly cookies change that boundary because JavaScript cannot read the values they hold. An injected script cannot use the localStorage theft path to take the cookie and replay it later. A live payload can still abuse the active browser session, but blocking direct token extraction narrows the compromise. Incident response must distinguish in-session abuse from a stolen bearer token that remains usable outside the browser while it is valid and has not been revoked.
What happens when an extractable key reaches a cloud credential
A user token and a cloud credential can be kept in the same unsafe place yet give access to very different things. The token may expose one user’s session. A cloud credential’s reach depends on the IAM policy attached to it. Extracting the string can therefore turn a client-side weakness into access to backend infrastructure.
Depending on those permissions, an attacker could read production databases, write to storage buckets, create or destroy infrastructure, or retrieve secrets from AWS Secrets Manager. A narrow policy limits what the attacker can reach, while broad permissions can let them use the same extracted value to access several systems.
Shared service credentials concentrate the exposure further. If many installations use one credential rather than per-user or per-device keys, extracting it from one copy can threaten resources used by others. The device becomes an entry point for loss that extends beyond it. The same authority may give access to stored data, live sessions, and backend trust relationships.
The storage locations that actually contain keys securely — and why they work
The remedy is to choose a boundary suited to the secret. Android Keystore protects cryptographic keys from export, with hardware-backed storage in a trusted execution environment or StrongBox where available. Access to the file system alone does not give someone the key. Access rules can also require user authentication or restrict cryptographic operations, limiting what compromised application code can ask the key to do. Weak, hardcoded, or reused key material still undermines that protection.
For iOS credentials, API tokens, and cryptographic material, the appropriate store is Keychain rather than NSUserDefaults or the documents directory. Keychain provides encrypted storage separate from the app’s file sandbox, with access controls against other apps. Backup behavior still needs deliberate attention. Items can survive app reinstalls and are included in encrypted backups by default, while ThisDeviceOnly accessibility variants exclude them from backup and migration. Protected credential storage is not the same property as a non-exportable cryptographic key.
For small Android data items, EncryptedSharedPreferences separates the protected file from its master key. The AndroidX Security Library encrypts values with AES-256-GCM and preference keys with AES-256-SIV, while the master key resides in Keystore. EncryptedSharedPreferences keeps copied preferences files encrypted and is appropriate for preferences and small data, with Keystore remaining appropriate for long-lived cryptographic secrets. Sensitive data should also be left out of backups through <fullBackupContent> and encrypted before it is stored.
For SPA session tokens, HttpOnly, Secure, SameSite cookies put the browser’s rules in charge of the values in place of application JavaScript. HttpOnly blocks script access to the value. Secure restricts transmission to HTTPS, while SameSite=Strict or Lax reduces cross-site request forgery exposure. Service credentials need to stay behind a separate boundary, in server-side environment variables or a secrets manager that supplies the backend at runtime. When an SPA needs a third-party API credential, a thin backend proxy should hold it and forward sanitized requests so the browser never receives the key.
Why static code review consistently misses hardcoded keys — and what actually finds them
Source review examines code in the context of a change, often one file at a time. That makes a lone credential easy to overlook or accept as temporary. Configuration, generated resources, and third-party SDK initialization blocks can receive less scrutiny than application logic. The reviewer sees the change that was intended but may miss values that end up in the final product.
Artifact analysis changes the object under examination. Scanning the compiled binary or JavaScript bundle on every build can flag embedded credentials regardless of the source file that supplied them. It tests what users actually receive. That closes a gap in source review, though it does not establish everything a discovered key can authorize. A suspicious string and a demonstrated compromise are different findings.
A hands-on penetration test establishes that next step by attempting exploitation within scope. Instead of stopping at “a key is present,” the tester can demonstrate whether it grants read access to an S3 bucket and the data inside. Testing can also uncover chained obstacles. Bypassing certificate pinning in the primary networking layer may reveal a second custom layer with its own check, beyond which previously invisible API calls become testable.
SPA testing needs the same attention to the client. Proxying API calls alone skips bundle secrets, client-side routing gaps, and DOM locations where injected scripts can execute. Crawlers that cannot execute JavaScript or interact with dynamic UI state also miss routes. Whitebox access to source, cloud configuration, and build output helps testers map parts of these paths they cannot see from outside. Combining artifact inspection with active testing gives more assurance than relying on source review or an automated scanner to cover everything.
Compliance evidence has to match the organization’s stated controls. If a control promises “annual penetration testing by a qualified third party,” a vulnerability scan report does not demonstrate that work. A scan identifies known issues automatically. A penetration test actively validates exploitability within scope, including logic flaws and chained attacks. That distinction gives an auditor evidence of what was tested.
Testing outside the SOC 2 observation window, omitting retesting, or choosing a scope that does not match the control wording can force work to be repeated. Missing remediation evidence creates another gap. For mobile and SPA products, API-only testing also leaves the binary, JavaScript bundle, local storage behavior, and backup configuration unexplored. In medical applications, exposed credentials or patient-data APIs can turn those omissions into HIPAA compliance risk.
For teams buying a test, the useful questions are concrete. They should establish how much work is manual, whether findings include working proof-of-concept exploits, whether client-side artifacts are in scope, and whether retesting is included. An attestation letter belongs alongside that evidence.
The positive case is that security and audit readiness need the same evidence. A test that shows an extraction path, records remediation, and retests the storage boundary helps engineers fix the weakness and auditors check the control. The fix is understood, and its implementation is testable. A proper assessment shows whether secrets remain protected in the product that ships.


