A hardcoded AES key like 'this-is-a-32-character-secret!!' passed to crypto.createCipheriv() is committed to source control and therefore permanently exposed to every developer who has ever had repo access, every CI system, every build log, and every git history tool. CWE-321 (Use of Hard-coded Cryptographic Key) and OWASP A02 (Cryptographic Failures) describe the consequence: if the key is compromised — and it is, the moment it was committed — every record encrypted with that key is decryptable by anyone with the history.
Medium because hardcoded crypto keys are permanently exposed in source history, but exploitation requires access to the codebase or ciphertext — not just network access.
Read the key from an environment variable and validate it at startup with a Zod schema to fail fast if it's missing or malformed.
// Bad: key is a string literal in source
const cipher = crypto.createCipheriv('aes-256-cbc', 'this-is-a-32-character-secret!!', iv)
// Good: key from env, validated at startup
import { z } from 'zod'
const Env = z.object({ ENCRYPTION_KEY: z.string().length(64) }) // 32 bytes = 64 hex chars
const env = Env.parse(process.env)
const key = Buffer.from(env.ENCRYPTION_KEY, 'hex')
const cipher = crypto.createCipheriv('aes-256-cbc', key, iv)
Generate a secure key with node -e "console.log(require('crypto').randomBytes(32).toString('hex'))" and store it in .env.local (never commit it). Rotate the key and re-encrypt all records if it was previously hardcoded.
ID: ai-slop-security-theater.half-wired-crypto.crypto-keys-from-env
Severity: medium
What to look for: Walk source files for calls to crypto.createCipheriv(, crypto.createDecipheriv(, crypto.createHmac(, crypto.scryptSync(, crypto.createSign(, crypto.createVerify(, nacl.secretbox(, nacl.box.keyPair.fromSecretKey(, aes.encrypt(, aes.decrypt(, Cipher( (CryptoJS). For each call site, before evaluating, extract and quote the key argument expression. Count all call sites where the key argument is a string literal of length 16/24/32/64 (likely hardcoded key) versus those where the key references process.env.X, env.X, or Buffer.from(process.env.X, 'hex').
Pass criteria: 100% of crypto calls source their key from environment variables. Report: "X crypto call sites, Y use env-based keys, 0 hardcoded."
Fail criteria: At least 1 crypto call passes a string literal as the key argument.
Skip (N/A) when: No crypto.createCipheriv / nacl / crypto-js calls anywhere in source.
Detail on fail: "1 hardcoded crypto key: src/lib/encrypt.ts line 8 calls crypto.createCipheriv('aes-256-cbc', 'this-is-a-32-character-secret!!', iv) — key is a literal, not from env"
Remediation: Hardcoded crypto keys are committed to source control and visible in every backup, log, and stack trace. Always read keys from env:
// Bad: hardcoded key
const cipher = crypto.createCipheriv('aes-256-cbc', 'this-is-a-32-character-secret!!', iv)
// Good: env-based key
const key = Buffer.from(process.env.ENCRYPTION_KEY!, 'hex')
const cipher = crypto.createCipheriv('aes-256-cbc', key, iv)
Use a typed env schema (Zod) to fail fast at startup if ENCRYPTION_KEY is missing.