From 94450969278d6af060a6a66dd7ca1cfa279c12ab Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Mon, 13 Jul 2026 22:37:25 +0000 Subject: [PATCH] crypto: cap GCM IV length at 128 bytes in createCipheriv Node.js (via OpenSSL 3's GCM provider) rejects GCM IVs longer than 1024 bits with ERR_CRYPTO_INVALID_IV. BoringSSL's EVP_CTRL_AEAD_SET_IVLEN has no such cap, so Bun accepted arbitrarily long IVs, turning an attacker-controlled IV length into unbounded GHASH work inside the constructor and producing ciphertext Node cannot decipher. Enforce the same 128-byte cap at the constructor for GCM ciphers. --- .../bindings/node/crypto/JSCipherConstructor.cpp | 10 ++++++++++ test/js/bun/crypto/cipheriv-decipheriv.test.ts | 13 +++++++++++++ .../parallel/test-crypto-cipheriv-decipheriv.js | 4 +++- 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/jsc/bindings/node/crypto/JSCipherConstructor.cpp b/src/jsc/bindings/node/crypto/JSCipherConstructor.cpp index fef1972bcea1..ff2c86cfc92e 100644 --- a/src/jsc/bindings/node/crypto/JSCipherConstructor.cpp +++ b/src/jsc/bindings/node/crypto/JSCipherConstructor.cpp @@ -189,6 +189,16 @@ JSC_DEFINE_HOST_FUNCTION(constructCipher, (JSC::JSGlobalObject * globalObject, J } } + // OpenSSL 3 caps GCM IVs at 1024 bits (GCM_IV_MAX_SIZE). BoringSSL has no + // such cap, so enforce it here to match Node.js and avoid unbounded GHASH work. + if (cipher.isGcmMode()) { + ASSERT(ivView); + + if (ivView->byteLength() > 128) { + return ERR::CRYPTO_INVALID_IV(scope, globalObject); + } + } + CipherCtxPointer ctx = CipherCtxPointer::New(); if (cipher.isWrapMode()) { diff --git a/test/js/bun/crypto/cipheriv-decipheriv.test.ts b/test/js/bun/crypto/cipheriv-decipheriv.test.ts index af8211d02348..0c1a427fda99 100644 --- a/test/js/bun/crypto/cipheriv-decipheriv.test.ts +++ b/test/js/bun/crypto/cipheriv-decipheriv.test.ts @@ -117,6 +117,19 @@ it("only zero-sized iv or null should be accepted in ECB mode", () => { it("should allow only valid iv lengths in GCM mode", () => { expect(sampleEncryptDecryptGCM("aes-256-gcm", randomBytes(32), randomBytes(1))).toBe(true); expect(sampleEncryptDecryptGCM("aes-256-gcm", randomBytes(32), randomBytes(96))).toBe(true); + expect(sampleEncryptDecryptGCM("aes-256-gcm", randomBytes(32), randomBytes(128))).toBe(true); +}); + +it("should reject GCM IVs longer than 128 bytes", () => { + // Node.js (OpenSSL 3) caps GCM IV length at 1024 bits / 128 bytes. + const invalidIV = { code: "ERR_CRYPTO_INVALID_IV" }; + for (const algo of ["aes-128-gcm", "aes-192-gcm", "aes-256-gcm"] as const) { + const key = randomBytes(algo === "aes-128-gcm" ? 16 : algo === "aes-192-gcm" ? 24 : 32); + expect(() => createCipheriv(algo, key, Buffer.alloc(128))).not.toThrow(); + expect(() => createCipheriv(algo, key, Buffer.alloc(129))).toThrow(expect.objectContaining(invalidIV)); + expect(() => createCipheriv(algo, key, Buffer.alloc(4096))).toThrow(expect.objectContaining(invalidIV)); + expect(() => createDecipheriv(algo, key, Buffer.alloc(129))).toThrow(expect.objectContaining(invalidIV)); + } }); const referencePlaintext = "Out of the mountain of despair, a stone of hope."; diff --git a/test/js/node/test/parallel/test-crypto-cipheriv-decipheriv.js b/test/js/node/test/parallel/test-crypto-cipheriv-decipheriv.js index 095458e7d0b4..6e3777460ac3 100644 --- a/test/js/node/test/parallel/test-crypto-cipheriv-decipheriv.js +++ b/test/js/node/test/parallel/test-crypto-cipheriv-decipheriv.js @@ -208,7 +208,9 @@ assert.throws( // But all other IV lengths should be accepted. const minIvLength = hasOpenSSL3 ? 8 : 1; -const maxIvLength = hasOpenSSL3 ? 64 : 256; +// Bun: BoringSSL has no upper bound, but Bun enforces the OpenSSL 3 cap of +// 128 bytes (1024 bits) for Node.js compatibility. +const maxIvLength = hasOpenSSL3 ? 64 : 129; for (let n = minIvLength; n < maxIvLength; n += 1) { if (isFipsEnabled && n < 12) continue; crypto.createCipheriv('aes-128-gcm', Buffer.alloc(16), Buffer.alloc(n));