Skip to main content
interlace
Plugin: node-securityRules

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.

📚 References

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>/environ by 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 storagelocalStorage, 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.

CodeWhy 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_TOKENA read, not a write.
process.env.API_TOKEN = encrypt(raw)Encrypted on the way in.
process.env[dynamicKey] = vA 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.