VNX-323: Reusing a Nonce, Key Pair in Encryption
An IV or nonce generated once (module, package or class level, or in a constructor) and reused by the encryption call for every message.
| Field | Value |
|---|---|
| Rule ID | VNX-323 |
| Severity | Medium |
| SARIF level | warning |
| Kind | sast |
| Languages | Go, Java, JavaScript / TypeScript, Python |
| CWE | CWE-323 |
| CAPEC | CAPEC-66 |
| ATT&CK | T1190 |
| CWSS vector | CWSS:1.0/TI:M/AP:A/AL:A/IC:N/FC:LT/RP:N/RL:A/AV:I/AS:N/IN:A/SC:A/BI:M/DI:H/EX:M/EC:N/P:C |
| Tags | nonce-reuse cwe-323 |
| Source | internal/sast/rules/vnx-323.rego |
Overview
VNX-323 maps to CWE-323: Reusing a Nonce, Key Pair in Encryption. It reports an encryption call inside a function or method whose IV or nonce argument was created once and is never refreshed there:
- a Python module-level variable assigned from
os.urandom,get_random_bytesorsecrets.token_bytes, or aself.attribute set in__init__; - a JavaScript/TypeScript module-level variable assigned from
crypto.randomBytesorgetRandomValues, or athis.property set in the constructor; - a Go package-level variable, or a struct field declared as
[]byte; - a Java
byte[]field, used in anENCRYPT_MODEinitialisation.
Sinks: Python AESGCM/ChaCha20Poly1305/AESCCM .encrypt(nonce, ...), pycryptodome .new(..., nonce=/iv=), cryptography modes.CBC/CTR/GCM/CFB/OFB; Node crypto.createCipheriv(alg, key, iv) and WebCrypto { name: 'AES-GCM', iv }; Go AEAD .Seal(dst, nonce, ...) and cipher.NewCBCEncrypter/NewCTR/NewOFB/NewCFBEncrypter; Java IvParameterSpec, GCMParameterSpec and ChaCha20ParameterSpec.
Not reported: an IV or nonce assigned, filled (rand.Read, io.ReadFull, nextBytes, randomFillSync, getRandomValues) or incremented inside the encrypting function; decryption calls; test files. Hard-coded constant IVs are VNX-CRYPTO-010’s concern. Flows through helpers and across files are not followed.
Why This Matters
A random nonce generated at start-up is still one nonce. Every message encrypted under the same key reuses it: with GCM, CTR and ChaCha20-Poly1305 this leaks the XOR of plaintexts and lets an attacker forge authenticated messages; with CBC it reveals which messages share a prefix.
What Gets Flagged
# FLAGGED
NONCE = os.urandom(12)
def seal(data):
return AESGCM(KEY).encrypt(NONCE, data, None)
// FLAGGED
const iv = crypto.randomBytes(16);
function encrypt(text) {
const cipher = crypto.createCipheriv('aes-256-ctr', key, iv);
// FLAGGED
func (s *Sealer) Encrypt(dst, src []byte) {
stream := cipher.NewCTR(s.block, s.iv)
Not flagged: nonce = os.urandom(12) inside the function before encrypt(nonce, ...), random.nextBytes(iv) before new GCMParameterSpec(128, iv), io.ReadFull(rand.Reader, b.nonce) before Seal.
Remediation
- Generate a fresh random nonce for every encryption (or use a counter that never repeats under one key) and send it with the ciphertext.
- Prefer a misuse-resistant AEAD (AES-GCM-SIV, XChaCha20-Poly1305 with random 192-bit nonces) when nonce management is hard.
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-323
// nosec vnx-323
Or record an ignore rule: vulnetix ignore add --rule vnx-323 --reason "...". See Suppressing findings.