From edec3daa6bc9dd7a06656a0391036f74b786cbac Mon Sep 17 00:00:00 2001 From: iaohkut Date: Sun, 12 Jul 2026 13:18:17 -0400 Subject: [PATCH] Fix CTR mode keystream reuse when IV is shorter than block size (CWE-323) CryptoJS.mode.CTR.processBlock() derives the counter from this._iv.slice(0) without padding it to the cipher's block size. When a caller passes a short IV (e.g. a standard 12-byte/96-bit nonce instead of a full 16-byte counter block, per NIST SP 800-38A's recommended nonce+counter construction), counter[blockSize - 1] is `undefined` after the first block. `(undefined + 1) | 0` evaluates to 0 in JS - coincidentally the same value implicitly used (via bitwise coercion of the missing array slot) as that word during the first block's keystream generation. So blocks 1 and 2 are encrypted with an *identical* keystream before the counter starts incrementing correctly from block 3 onward - a critical two-time-pad-style keystream reuse that lets an attacker recover the XOR of the first two plaintext blocks from their ciphertexts. Already publicly reported and root-caused in https://github.com/brix/crypto-js/issues/508 with a working PoC; this commit provides the fix. CTR-Gladman mode (mode-ctr-gladman.js) was checked and is NOT affected - it increments the counter's low-order word (index 0) first, which always exists regardless of IV length. Fix: zero-pad the counter array up to blockSize immediately after copying the IV, so counter[blockSize - 1] is always a defined 0 (matching the well-known IV-padding workaround from the issue thread) rather than undefined. Verified against the official test/mode-ctr-test.js vectors (full 16-byte IV, unaffected/unchanged) and issue #508's own reproduction case (12-byte nonce now produces ciphertext identical to a manually zero-padded 16-byte IV). Co-Authored-By: iaohkut --- src/mode-ctr.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/mode-ctr.js b/src/mode-ctr.js index 59c8bff..3b2615e 100644 --- a/src/mode-ctr.js +++ b/src/mode-ctr.js @@ -16,6 +16,20 @@ CryptoJS.mode.CTR = (function () { if (iv) { counter = this._counter = iv.slice(0); + // Zero-pad a short IV (e.g. a 12-byte/3-word nonce) up to + // the cipher's full block size. Without this, counter[blockSize - 1] + // is `undefined` after the first block, and `undefined + 1 | 0` + // evaluates to 0 - the same value implicitly used (via bitwise + // coercion) as the missing word during that first block's + // keystream generation. That makes the counter appear + // unchanged for one extra block, so blocks 1 and 2 are + // encrypted with an identical keystream (a critical CTR-mode + // keystream-reuse break) before incrementing correctly from + // block 3 onward. + for (var i = counter.length; i < blockSize; i++) { + counter[i] = 0; + } + // Remove IV for subsequent blocks this._iv = undefined; }