Plaintext PII in chrome.storage.local, chrome.storage.sync, localStorage, or IndexedDB is readable by any code with access to the extension's storage context — including a compromised extension update, a malicious dependency, or a script that gains access via a content script bridge. GDPR Art. 32 requires appropriate technical measures to protect personal data; storing an email address or user profile in cleartext fails that standard. CWE-312 (Cleartext Storage of Sensitive Information) and OWASP 2021 A02 (Cryptographic Failures) both target this exact pattern. NIST SP 800-53 SC-28 mandates data at rest protection for sensitive records.
High because plaintext PII in extension storage is readable by any compromised extension update or malicious dependency that gains access to the storage context — encryption is the only mitigation that survives a supply chain compromise.
Encrypt PII before writing it to any storage mechanism. Use TweetNaCl.js for symmetric encryption:
import nacl from 'tweetnacl';
import { encodeBase64, decodeUTF8 } from 'tweetnacl-util';
const nonce = nacl.randomBytes(nacl.secretbox.nonceLength);
const keyMaterial = await deriveKey(masterPassword); // scrypt or PBKDF2
const encrypted = nacl.secretbox(
decodeUTF8('user@example.com'),
nonce,
keyMaterial
);
chrome.storage.local.set({
encryptedEmail: encodeBase64(encrypted),
emailNonce: encodeBase64(nonce)
});
For lighter-weight cases, crypto-js provides AES-256 with a simpler API. Never store the encryption key alongside the ciphertext in the same storage location.
ID: extension-data-privacy.privacy-disclosures.local-pii-encrypted
Severity: high
What to look for: First, determine whether the extension stores any PII locally at all (via chrome.storage.local, chrome.storage.sync, localStorage, or IndexedDB). PII examples: email address, full name, username, profile data, preferences linked to identity, browsing history associated with a user. API keys and service credentials alone are NOT PII. If PII is stored locally, check whether it is encrypted before storage.
Skip (N/A) when: The extension does not store any PII locally — it either sends data to external servers without local persistence, or only stores non-PII data such as API keys or anonymous settings. Evaluate skip BEFORE pass/fail: if no local PII storage exists, mark skip regardless of encryption patterns.
Pass criteria: Enumerate all local storage writes that contain PII fields. Confirmed PII is stored locally AND 100% of PII fields are encrypted (e.g., using TweetNaCl.js, libsodium.js, or similar). At minimum, sensitive fields are obfuscated or hashed.
Fail criteria: Confirmed PII found stored in plaintext in chrome.storage.local, chrome.storage.sync, localStorage, or IndexedDB. No encryption or hashing applied.
Detail on fail: Specify the plaintext PII. Example: "User email stored in chrome.storage.local without encryption: chrome.storage.local.set({ userEmail: 'user@example.com' })" or "User profile data cached as plaintext JSON in localStorage."
Remediation: Encrypt sensitive data before storage:
import nacl from 'tweetnacl';
const secretKey = ...; // Store securely, e.g., derived from master password
const plaintext = 'user@example.com';
const encrypted = nacl.secretbox(
nacl.util.decodeUTF8(plaintext),
nonce,
secretKey
);
chrome.storage.local.set({ encryptedEmail: nacl.util.encodeBase64(encrypted) });
Or use a library like crypto-js for simpler AES encryption.