require-secure-credential-storage
This rule detects a credential written to localStorage, sessionStorage, AsyncStorage or process.env without encryption
Enforces secure storage patterns for credentials
Severity: 🔴 CRITICAL
CWE: CWE-312: Cleartext Storage of Sensitive Information
OWASP Mobile: M1: Improper Credential Usage
Rule Details
This rule detects when a credential is stored via setItem() on client persistent storage — localStorage, sessionStorage, React Native's AsyncStorage — or assigned into process.env, without encryption. Insecure credential storage (plaintext, weak encryption) leads to credential theft if the device is compromised, local storage is read, or the environment is inherited by a child process.
fs.writeFile() is not this rule; disk writes belong to require-storage-encryption.
Why This Matters
Stored credentials must be encrypted to prevent theft:
- Device theft: Attackers access unencrypted storage on stolen devices
- Malware: Keyloggers or storage scanners extract plaintext credentials
- Forensics: Deleted plaintext files can be recovered
- Compliance: GDPR/PCI-DSS require encryption for stored credentials
❌ Incorrect
// Plaintext localStorage (browser)
localStorage.setItem('authToken', user.token); // ❌ Unencrypted
// Plaintext file storage (Node.js)
import fs from 'fs';
fs.writeFile(
'credentials.json',
JSON.stringify({
username: user.username,
password: user.password, // ❌ Plaintext password!
}),
);
// Base64 encoding (NOT encryption!)
const encoded = btoa(JSON.stringify(credentials));
localStorage.setItem('creds', encoded); // ❌ Still plaintext, just encoded
// Weak "encryption" with reversible encoding
const obfuscated = rot13(password);
fs.writeFileSync('pass.txt', obfuscated); // ❌ Trivially reversible✅ Correct
const x = 42;Known False Negatives
The following patterns are not detected due to static analysis limitations:
Encryption via Wrapper Functions
Why: We only detect a direct setItem() call whose VALUE argument is an encryption call. If encryption happens inside a wrapper function, we cannot verify it.
// ❌ NOT DETECTED - Wrapper may or may not encrypt
function saveCredentials(creds: Credentials) {
localStorage.setItem('creds', JSON.stringify(creds)); // Actually unencrypted!
}
saveCredentials({ username, password });Mitigation: Document encryption requirements for wrapper functions. Use TypeScript branded types for encrypted data.
Weak or Broken Encryption
Why: We only check for the presence of encrypt() in the call chain. We can't verify encryption strength.
// ❌ NOT DETECTED - Weak encryption
const weakEncrypted = xorEncrypt(password, 'key'); // XOR is broken
localStorage.setItem('pass', weakEncrypted);Mitigation: Use vetted encryption libraries (SubtleCrypto, Node crypto). Enforce AES-256-GCM minimum.
IndexedDB
Why: localStorage, sessionStorage and AsyncStorage are analyzed. IndexedDB is not.
// ❌ NOT DETECTED - IndexedDB
store.put({ id: 1, token: authToken }); // Still unencrypted!Mitigation: Apply the encryption requirement to all persistent storage APIs.
⚙️ Configuration
This rule has no configuration options. It requires encrypt() wrapper for all setItem() and writeFile() calls.
🔗 Related Rules
no-hardcoded-credentials- Prevent hardcoded passwordsrequire-storage-encryption- General storage encryption
📚 References
- CWE-312: Cleartext Storage
- OWASP Mobile M1: Improper Credential Usage
- Web Crypto API
- Node.js Crypto Module
The Node sink: process.env
// ❌ CWE-526 — a credential assigned into the environment
process.env.SESSION_TOKEN = sessionToken;
process.env.NPM_TOKEN = await vault.read('npm/token');Everything the rule used to check — localStorage, sessionStorage,
AsyncStorage — is a browser or React Native global. None of them exists in
Node, so on a pure server codebase this rule had no reachable sink at all: it
was a browser rule filed under Node, quiet on every project it ran against.
Disk writes could not simply be added here;
require-storage-encryption owns those, and
duplicating them recreates the double-reporting defect the two rules were split
apart to fix. process.env is the sink neither rule claimed, and it is the one
Node actually has.
Why the assignment is a finding:
- every child process the app spawns inherits the value;
- it is readable at
/proc/<pid>/environby anything running as the same user; - crash dumps and the environment snapshots error reporters upload capture it verbatim.
Reading process.env.TOKEN is fine and universal — only the write reports.
Not a finding
This rule owns client persistent storage — localStorage, sessionStorage and React
Native's AsyncStorage, all of which keep what you give them in the clear — plus
the Node environment. Writes to disk belong to
require-storage-encryption.
| Code | Why it is silent |
|---|---|
localStorage.setItem('theme', 'dark') | A store, but nothing says a credential is going into it. |
localStorage.setItem('authToken', encrypt(token)) | Encrypted on the way in. |
cache.setItem('password', pwd) | setItem on something that is not a persistent store. |
localStorage.setItem('key', publicKey) | key alone is not evidence — it matches keyboard, keyCode, objectKey. |
process.env.PORT = '3000' | The overwhelmingly common use of this assignment. No credential named. |
const t = process.env.AUTH_TOKEN | A read, not a write. |
process.env.API_TOKEN = encrypt(raw) | Encrypted on the way in. |
process.env[dynamicKey] = v | A computed key with no readable name is no evidence. |
If it fires, the key or the value named a credential. Note that an encrypt-looking
variable is not proof: setItem('authToken', encrypted) still reports, because nothing
in the file shows anything encrypted it. Wrap the value in the encryption call and the
rule goes quiet.
Did this rule catch something? Star the repo to get new CWE coverage as we ship it — or follow the AI-code-security benchmarks behind these rules.