VNX-321: Hard-coded cryptographic key
A cryptographic key written into the source: a key-named variable assigned key material, a literal assigned to a variable that a key API consumes, or a literal passed straight to AES/DES/HMAC/Fernet/SecretKeySpec/createCipheriv/NewCipher or a JWT call. Node JWT, session and HMAC signing keys are reported by VNX-NODE-019.
| Field | Value |
|---|---|
| Rule ID | VNX-321 |
| Severity | High |
| SARIF level | error |
| Kind | sast |
| Languages | Go, Java, JavaScript / TypeScript, Python |
| CWE | CWE-321 |
| CAPEC | CAPEC-66 |
| ATT&CK | T1190 |
| CWSS vector | CWSS:1.0/TI:H/AP:A/AL:A/IC:N/FC:LT/RP:N/RL:A/AV:I/AS:N/IN:A/SC:A/BI:H/DI:H/EX:H/EC:N/P:H |
| Tags | crypto hardcoded-key cwe-321 |
| Source | internal/sast/rules/vnx-321.rego |
Overview
VNX-321 maps to CWE-321: Use of Hard-coded Cryptographic Key. It reports, in Python, JavaScript/TypeScript, Java and Go:
- a variable, constant, field or config key named as a cryptographic key (
secret_key,private_key,encryption_key,signing_key,hmac_key,aes_key,des_key,master_key,crypto_key,cipher_key,enc_key,jwt_secret) assigned a literal that looks like key material: at least 16 characters, no spaces, not a placeholder or an environment lookup; - a string or byte literal passed straight to a key API: pycryptodome
AES/DES/ARC4.new(), cryptographyFernet/AESGCM/ChaCha20Poly1305/algorithms.AES,hmac.new, Nodecrypto.createCipheriv/createDecipheriv/createHmac, CryptoJSAES/DES/TripleDES/Rabbit/RC4/Hmac*, Javanew SecretKeySpec("...".getBytes(), ...)and byte-array literals, Goaes.NewCipher/des.NewCipher/hmac.Newwith[]byte("...").
Reported by VNX-NODE-019 instead: in JavaScript and TypeScript, a literal key given to jwt.sign()/jwt.verify() or crypto.createHmac(alg, '...'), a config property named for a JWT or session secret (jwtSecret, jwt_secret) set to a literal, and a constant holding a literal that one of those calls uses as its key. That rule is the Node-specific form of this one, so each such key is one finding. In an examples/ directory, which VNX-NODE-019 skips, this rule still reports them. The other JavaScript shapes (createCipheriv, CryptoJS, jwt.encode/decode, key-named properties such as cryptoKey) stay here.
Not reported: names that merely end in key (map keys, cache keys, loop variables), API keys and passwords (credentials, covered by the secret rules), placeholders (changeme, your-..., example, xxxx, ${...}), environment and config lookups, short non-key strings assigned to key names, test files and comments.
Why This Matters
A key in the source is in every clone, build artefact and backup of the repository. Anyone who has it can decrypt the data it protects or forge the signatures and MACs it produces, and it cannot be rotated without a release.
What Gets Flagged
# FLAGGED
cipher = AES.new(b"0123456789abcdef", AES.MODE_GCM)
SECRET_KEY = "9f8a7b6c5d4e3f2a1b0c9d8e7f6a5b4c"
key = b"Rq7Lx2Mz9Pk4Wn8T" # FLAGGED: consumed as the key below
cipher = AES.new(key, AES.MODE_GCM)
// FLAGGED
crypto.createCipheriv('aes-256-cbc', 'a3f9c2e17b8d4065a3f9c2e17b8d4065', iv)
const config = { cryptoKey: "c0ffee00deadbeefc0ffee00deadbeef" }
// Reported by VNX-NODE-019, not here
crypto.createHmac('sha256', 'webhook-signing-secret')
jwt.sign({ sub: user.id }, 'Zq8vR2mK9pL4xW7nB3cT6yH1')
// FLAGGED
b, _ := aes.NewCipher([]byte("0123456789abcdef0123456789abcdef"))
Not flagged: const ENCRYPTION_KEY = process.env.ENCRYPTION_KEY, cache_key = "user:profile:1234567890abcdef", secretKey: "your-secret-key-goes-here-please".
Remediation
- Load keys at runtime from a secrets manager or KMS, or derive them from a secret that is.
- Rotate any key that has been committed: it must be treated as public.
References
Suppressing this rule
Put a nosec comment on the line, or on line 1 to cover the whole file. Use the comment marker of the file’s language (#, //, -- or ;):
# nosec vnx-321
// nosec vnx-321
Or record an ignore rule: vulnetix ignore add --rule vnx-321 --reason "...". See Suppressing findings.