Severity: Critical
File: backend/src/lib/userApiKeys.ts:39-48
CWE: CWE-916 — Use of Password Hash With Insufficient Computational Effort
OWASP: A02:2021 — Cryptographic Failures
Description
The AES-256-GCM encryption key for stored user API keys is derived with a raw SHA-256 hash — no KDF, no salt, no iteration count:
function encryptionKey(): Buffer {
const secret = process.env.USER_API_KEYS_ENCRYPTION_SECRET || ...
return crypto.createHash("sha256").update(secret).digest();
}
Impact
Anyone who obtains the secret computes the identical AES key in a single SHA-256 operation and immediately decrypts every row in user_api_keys. There is no per-entry salt, so all rows are decryptable in one pass — no per-entry brute force required.
Users store Anthropic, OpenAI, and Gemini API keys here. These keys have direct monetary value and can be used to run inference workloads billed to the victim.
Fix
Replace SHA-256 with HKDF (RFC 5869) and store a per-row random salt:
import { hkdfSync, randomBytes } from "crypto";
function deriveKey(secret: string, salt: Buffer): Buffer {
return Buffer.from(hkdfSync("sha256", secret, salt, "mike-api-keys-v1", 32));
}
function encrypt(value: string): Omit<EncryptedKeyRow, "provider"> {
const salt = randomBytes(16);
const key = deriveKey(getEncryptionSecret(), salt);
const iv = randomBytes(12);
const cipher = createCipheriv("aes-256-gcm", key, iv);
const encrypted = Buffer.concat([cipher.update(value, "utf8"), cipher.final()]);
return {
encrypted_key: encrypted.toString("base64"),
iv: iv.toString("base64"),
auth_tag: cipher.getAuthTag().toString("base64"),
salt: salt.toString("base64"),
};
}
A salt column must be added to the user_api_keys table. Existing rows will need re-encryption during migration.
Remediation tier: Immediate — block all production deploys until resolved.
Severity: Critical
File:
backend/src/lib/userApiKeys.ts:39-48CWE: CWE-916 — Use of Password Hash With Insufficient Computational Effort
OWASP: A02:2021 — Cryptographic Failures
Description
The AES-256-GCM encryption key for stored user API keys is derived with a raw SHA-256 hash — no KDF, no salt, no iteration count:
Impact
Anyone who obtains the secret computes the identical AES key in a single SHA-256 operation and immediately decrypts every row in
user_api_keys. There is no per-entry salt, so all rows are decryptable in one pass — no per-entry brute force required.Users store Anthropic, OpenAI, and Gemini API keys here. These keys have direct monetary value and can be used to run inference workloads billed to the victim.
Fix
Replace SHA-256 with HKDF (RFC 5869) and store a per-row random salt:
A
saltcolumn must be added to theuser_api_keystable. Existing rows will need re-encryption during migration.Remediation tier: Immediate — block all production deploys until resolved.