Skip to content

Encryption key extractable from sessionStorage via XSS #9

@sgardoll

Description

@sgardoll

Summary

The AES-256-GCM encryption key protecting API keys is stored as an exportable JWK in sessionStorage. The device fingerprint for key derivation consists entirely of publicly observable browser properties. If any XSS vulnerability is exploited, all stored API keys can be decrypted.

Risk Assessment

  • Risk Level: High
  • Likelihood: Conditional (requires XSS)
  • Impact: Critical — full API key exfiltration
  • Timeline: If XSS is exploited

Affected Code

Key storage (app.js lines 264-266)

const exportedKey = await crypto.subtle.exportKey("jwk", key);
sessionStorage.setItem(ENCRYPTION_KEY_NAME, JSON.stringify(exportedKey));

Predictable fingerprint (app.js lines 276-282)

All 5 fingerprint components (userAgent, language, screen size, timezone, cores) are publicly observable and deterministic.

Attack Chain

  1. Exploit any XSS vector
  2. JSON.parse(sessionStorage.getItem("ccc_encryption_key")) → JWK
  3. Import key, decrypt all ccc_api_key_* from localStorage
  4. Exfiltrate all API keys

Suggested Fix

Primary: Fix XSS vulnerabilities first

Defense-in-depth: Non-extractable keys

// Change exportable from true to false on line 260
const key = await crypto.subtle.deriveKey(
  { name: "PBKDF2", salt, iterations: 100000, hash: "SHA-256" },
  keyMaterial,
  { name: "AES-GCM", length: 256 },
  false,  // non-extractable
  ["encrypt", "decrypt"],
);

Key would need to be re-derived each session from fingerprint + salt instead of stored.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingsecuritySecurity vulnerability

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions