From e4108363b1c4763ba338c6906730a1ba9ef317c4 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Fri, 7 Aug 2026 15:06:25 +0000 Subject: [PATCH] crypto: prevent Hmac.digest() from returning uninitialized memory Hmac.prototype._flush was aliased to Hash.prototype._flush, which finalizes the native HMAC context but never sets the JavaScript-side kFinalized flag. After an Hmac has been used as a stream, a subsequent Hmac.prototype.digest() call therefore still believes the object has not been finalized and calls into C++ a second time. On that second call the native context has already been reset, so the digest buffer is never written and Digest::MAX_SIZE bytes of uninitialized stack memory are returned to JavaScript. Hash is not affected because Hash::HashDigest caches its digest (refs #28245); Hmac never received the equivalent protection. Give Hmac its own _flush that sets kFinalized so repeat digest() calls after stream use are handled by the existing DEP0206 guard. As defense in depth, also set buf.len = 0 on the native side when the context has already been reset so unwritten bytes can never be emitted. --- lib/internal/crypto/hash.js | 6 +++++- src/crypto/crypto_hmac.cc | 3 +++ test/parallel/test-crypto-hmac.js | 20 ++++++++++++++++++++ 3 files changed, 28 insertions(+), 1 deletion(-) diff --git a/lib/internal/crypto/hash.js b/lib/internal/crypto/hash.js index 16834f169a5b..b879d467b0c6 100644 --- a/lib/internal/crypto/hash.js +++ b/lib/internal/crypto/hash.js @@ -187,7 +187,11 @@ Hmac.prototype.digest = function digest(outputEncoding) { return ret; }; -Hmac.prototype._flush = Hash.prototype._flush; +Hmac.prototype._flush = function _flush(callback) { + this.push(this[kHandle].digest()); + this[kState][kFinalized] = true; + callback(); +}; Hmac.prototype._transform = Hash.prototype._transform; // Implementation for WebCrypto subtle.digest() diff --git a/src/crypto/crypto_hmac.cc b/src/crypto/crypto_hmac.cc index 42f3b53da0ea..c8328f8ec4fd 100644 --- a/src/crypto/crypto_hmac.cc +++ b/src/crypto/crypto_hmac.cc @@ -141,6 +141,9 @@ void Hmac::HmacDigest(const FunctionCallbackInfo& args) { return ThrowCryptoError(env, ERR_get_error(), "Failed to finalize HMAC"); } hmac->ctx_.reset(); + } else { + // The context has already been finalized; never emit unwritten bytes. + buf.len = 0; } Local ret; diff --git a/test/parallel/test-crypto-hmac.js b/test/parallel/test-crypto-hmac.js index 9ddc4a4b880f..d5c16d9aa834 100644 --- a/test/parallel/test-crypto-hmac.js +++ b/test/parallel/test-crypto-hmac.js @@ -296,6 +296,26 @@ for (let i = 0, l = rfc4231.length; i < l; i++) { } } +// Calling digest() after the Hmac has already been used as a stream must +// return an empty buffer (the DEP0206 repeat-digest guard), not uninitialized +// stack memory. The stream itself must still produce the correct digest. +// See: https://github.com/nodejs/node/issues/28245 +{ + const key = 'key'; + const data = 'some data to hash'; + + const streamHmac = crypto.createHmac('sha256', key); + streamHmac.end(data); + const streamDigest = streamHmac.read(); + + // digest() after the stream already finalized must not return garbage. + assert.deepStrictEqual(streamHmac.digest(), Buffer.from('')); + + // Sanity check: the stream itself produced the correct digest. + const expected = crypto.createHmac('sha256', key).update(data).digest(); + assert.deepStrictEqual(streamDigest, expected); +} + // Test HMAC-MD5/SHA1 (rfc 2202 Test Cases) const rfc2202_md5 = [ {