auth0/node-jws improper HMAC signature verification vulnerability
Description
auth0/node-jws is a JSON Web Signature implementation for Node.js. In versions 3.2.2 and earlier and version 4.0.0, auth0/node-jws has an improper signature verification vulnerability when using the HS256 algorithm under specific conditions. Applications are affected when they use the jws.createVerify() function for HMAC algorithms and use user-provided data from the JSON Web Signature protected header or payload in HMAC secret lookup routines, which can allow attackers to bypass signature verification. This issue has been patched in versions 3.2.3 and 4.0.1.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
CVE-2025-65945 describes an improper HS256 signature verification in auth0/node-jws ≤3.2.2/4.0.0, allowing attackers to bypass HMAC validation using crafted secret lookups.
Root
Cause CVE-2025-65945 affects the jws.createVerify() function in the auth0/node-jws library when used with the HS256 HMAC algorithm. The vulnerability occurs when the secret lookup routine incorporates user-controlled data from the JWS protected header or payload. Under these specific conditions, the signature verification logic does not properly validate the HMAC, allowing an attacker to craft a token that will be accepted as valid [1].
Attack
Vector Exploitation requires that the application uses jws.createVerify() for HMAC-based algorithms and that the HMAC secret is derived from user-supplied values in the JWS header or payload. An attacker in a network position to inject a crafted JWT can supply a specially constructed token with arbitrary header or payload values that trick the secret lookup into using a weak or empty secret. The commit history shows tests verifying that empty secret buffers and empty secret strings are now properly rejected, indicating prior acceptance of such weak secrets [3][4].
Impact
A successful exploit allows the attacker to bypass HMAC signature verification entirely. They can forge a valid JWT for any payload, leading to authentication bypass, privilege escalation, or other actions relying on the integrity of the JWT. The vulnerability is specific to the streaming verify API and HMAC algorithms, not affecting RSA/ECDSA or the synchronous jws.verify() call [2].
Mitigation
The issue is patched in versions 3.2.3 and 4.0.1 [1]. Users should upgrade immediately. As a workaround, applications should avoid using user-provided data to determine the HMAC secret and ensure secret validation is independent of token contents.
AI Insight generated on May 19, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
jwsnpm | < 3.2.3 | 3.2.3 |
jwsnpm | >= 4.0.0, < 4.0.1 | 4.0.1 |
Affected products
2- auth0/node-jwsv5Range: < 3.2.3
Patches
26 files changed · +146 −6
CHANGELOG.md+15 −2 modified@@ -1,8 +1,20 @@ # Change Log + All notable changes to this project will be documented in this file. +## [3.2.3] + +### Changed + +- Fix advisory GHSA-869p-cjfg-cm3x: createSign and createVerify now require + that a non empty secret is provided (via opts.secret, opts.privateKey or opts.key) + when using HMAC algorithms. +- Upgrading JWA version to 1.4.2, adressing a compatibility issue for Node >= 25. + ## [3.0.0] + ### Changed + - **BREAKING**: `jwt.verify` now requires an `algorithm` parameter, and `jws.createVerify` requires an `algorithm` option. The `"alg"` field signature headers is ignored. This mitigates a critical security flaw @@ -12,7 +24,9 @@ All notable changes to this project will be documented in this file. for details. ## [2.0.0] - 2015-01-30 + ### Changed + - **BREAKING**: Default payload encoding changed from `binary` to `utf8`. `utf8` is a is a more sensible default than `binary` because many payloads, as far as I can tell, will contain user-facing @@ -21,14 +35,13 @@ All notable changes to this project will be documented in this file. - Code reorganization, thanks [@fearphage]! (<code>[7880050]</code>) ### Added + - Option in all relevant methods for `encoding`. For those few users that might be depending on a `binary` encoding of the messages, this is for them. (<code>[6b6de48]</code>) [unreleased]: https://github.com/brianloveswords/node-jws/compare/v2.0.0...HEAD [2.0.0]: https://github.com/brianloveswords/node-jws/compare/v1.0.1...v2.0.0 - [7880050]: https://github.com/brianloveswords/node-jws/commit/7880050 [6b6de48]: https://github.com/brianloveswords/node-jws/commit/6b6de48 - [@fearphage]: https://github.com/fearphage
.gitignore+1 −0 modified@@ -2,3 +2,4 @@ node_modules /test/keys /test/*.pem /test/encrypted-key-passphrase +package-lock.json
lib/sign-stream.js+6 −1 modified@@ -34,7 +34,12 @@ function jwsSign(opts) { } function SignStream(opts) { - var secret = opts.secret||opts.privateKey||opts.key; + var secret = opts.secret; + secret = secret == null ? opts.privateKey : secret; + secret = secret == null ? opts.key : secret; + if (/^hs/i.test(opts.header.alg) === true && secret == null) { + throw new TypeError('secret must be a string or buffer or a KeyObject') + } var secretStream = new DataStream(secret); this.readable = true; this.header = opts.header;
lib/verify-stream.js+6 −1 modified@@ -79,7 +79,12 @@ function jwsDecode(jwsSig, opts) { function VerifyStream(opts) { opts = opts || {}; - var secretOrKey = opts.secret||opts.publicKey||opts.key; + var secretOrKey = opts.secret; + secretOrKey = secretOrKey == null ? opts.publicKey : secretOrKey; + secretOrKey = secretOrKey == null ? opts.key : secretOrKey; + if (/^hs/i.test(opts.algorithm) === true && secretOrKey == null) { + throw new TypeError('secret must be a string or buffer or a KeyObject') + } var secretStream = new DataStream(secretOrKey); this.readable = true; this.algorithm = opts.algorithm;
package.json+2 −2 modified@@ -1,6 +1,6 @@ { "name": "jws", - "version": "3.2.2", + "version": "3.2.3", "description": "Implementation of JSON Web Signatures", "main": "index.js", "directories": { @@ -24,7 +24,7 @@ "readmeFilename": "readme.md", "gitHead": "c0f6b27bcea5a2ad2e304d91c2e842e4076a6b03", "dependencies": { - "jwa": "^1.4.1", + "jwa": "^1.4.2", "safe-buffer": "^5.0.1" }, "devDependencies": {
test/jws.test.js+116 −0 modified@@ -330,3 +330,119 @@ test('jws.isValid', function (t) { t.same(jws.isValid(valid), true); t.end(); }); + +test('Streaming sign: HMAC with empty secret buffer', function (t) { + const dataStream = readstream('data.txt'); + const secret = Buffer.alloc(0); + const sig = jws.createSign({ + header: { alg: 'HS256' }, + secret: secret + }); + dataStream.pipe(sig.payload); + sig.on('done', function (signature) { + t.ok(jws.verify(signature, 'HS256', secret), 'should verify'); + t.end(); + }).sign(); +}); + +test('Streaming sign: HMAC with empty secret string', function (t) { + const dataStream = readstream('data.txt'); + const secret = ''; + const sig = jws.createSign({ + header: { alg: 'HS256' }, + secret: secret + }); + dataStream.pipe(sig.payload); + sig.on('done', function (signature) { + t.ok(jws.verify(signature, 'HS256', secret), 'should verify'); + t.end(); + }).sign(); +}); + +test('Streaming sign: HMAC with undefined secret', function (t) { + try { + jws.createSign({ + header: { alg: 'HS256' }, + secret: undefined + }); + t.fail('should have errored'); + t.end(); + } catch (error) { + t.equal(error.name, 'TypeError'); + t.equal(error.message, 'secret must be a string or buffer or a KeyObject'); + t.end(); + } +}); + +test('Streaming sign: HMAC with null secret', function (t) { + try { + jws.createSign({ + header: { alg: 'HS256' }, + secret: null + }); + t.fail('should have errored'); + t.end(); + } catch (error) { + t.equal(error.name, 'TypeError'); + t.equal(error.message, 'secret must be a string or buffer or a KeyObject'); + t.end(); + } +}); + +test('Streaming verify: HMAC with empty secret buffer', function (t) { + const secret = Buffer.alloc(0); + jws.createVerify({ + signature: 'eyJhbGciOiJIUzI1NiJ9.b25lLCB0d28sIHRocmVlCg.V1oQ0aq6FgAoe7C2TORtYpQAbYzJoFNFZlJkTlF1e60', + algorithm: 'HS256', + secret: secret + }).on('done', function (valid, decoded) { + t.true(valid); + t.same(decoded.payload, readfile('data.txt')); + t.end(); + }).verify(); +}); + +test('Streaming verify: HMAC with empty secret string', function (t) { + const secret = ''; + jws.createVerify({ + signature: 'eyJhbGciOiJIUzI1NiJ9.b25lLCB0d28sIHRocmVlCg.V1oQ0aq6FgAoe7C2TORtYpQAbYzJoFNFZlJkTlF1e60', + algorithm: 'HS256', + secret: secret + }).on('done', function (valid, decoded) { + t.true(valid); + t.same(decoded.payload, readfile('data.txt')); + t.end(); + }).verify(); +}); + +test('Streaming verify: HMAC with undefined secret', function (t) { + try { + jws.createVerify({ + signature: 'eyJhbGciOiJIUzI1NiJ9.b25lLCB0d28sIHRocmVlCg.V1oQ0aq6FgAoe7C2TORtYpQAbYzJoFNFZlJkTlF1e60', + algorithm: 'HS256', + secret: undefined + }); + t.fail('should have errored'); + t.end(); + } catch (error) { + t.equal(error.name, 'TypeError'); + t.equal(error.message, 'secret must be a string or buffer or a KeyObject'); + t.end(); + } +}); + +test('Streaming verify: HMAC with null secret', function (t) { + try { + jws.createVerify({ + signature: 'eyJhbGciOiJIUzI1NiJ9.b25lLCB0d28sIHRocmVlCg.V1oQ0aq6FgAoe7C2TORtYpQAbYzJoFNFZlJkTlF1e60', + algorithm: 'HS256', + secret: null + }); + t.fail('should have errored'); + t.end(); + } catch (error) { + t.equal(error.name, 'TypeError'); + t.equal(error.message, 'secret must be a string or buffer or a KeyObject'); + t.end(); + } +});
6 files changed · +155 −6
CHANGELOG.md+24 −2 modified@@ -1,8 +1,29 @@ # Change Log + All notable changes to this project will be documented in this file. +## [4.0.1] + +### Changed + +- Fix advisory GHSA-869p-cjfg-cm3x: createSign and createVerify now require + that a non empty secret is provided (via opts.secret, opts.privateKey or opts.key) + when using HMAC algorithms. +- Upgrading JWA version to 2.0.1, adressing a compatibility issue for Node >= 25. + +## [3.2.3] + +### Changed + +- Fix advisory GHSA-869p-cjfg-cm3x: createSign and createVerify now require + that a non empty secret is provided (via opts.secret, opts.privateKey or opts.key) + when using HMAC algorithms. +- Upgrading JWA version to 1.4.2, adressing a compatibility issue for Node >= 25. + ## [3.0.0] + ### Changed + - **BREAKING**: `jwt.verify` now requires an `algorithm` parameter, and `jws.createVerify` requires an `algorithm` option. The `"alg"` field signature headers is ignored. This mitigates a critical security flaw @@ -12,7 +33,9 @@ All notable changes to this project will be documented in this file. for details. ## [2.0.0] - 2015-01-30 + ### Changed + - **BREAKING**: Default payload encoding changed from `binary` to `utf8`. `utf8` is a is a more sensible default than `binary` because many payloads, as far as I can tell, will contain user-facing @@ -21,14 +44,13 @@ All notable changes to this project will be documented in this file. - Code reorganization, thanks [@fearphage]! (<code>[7880050]</code>) ### Added + - Option in all relevant methods for `encoding`. For those few users that might be depending on a `binary` encoding of the messages, this is for them. (<code>[6b6de48]</code>) [unreleased]: https://github.com/brianloveswords/node-jws/compare/v2.0.0...HEAD [2.0.0]: https://github.com/brianloveswords/node-jws/compare/v1.0.1...v2.0.0 - [7880050]: https://github.com/brianloveswords/node-jws/commit/7880050 [6b6de48]: https://github.com/brianloveswords/node-jws/commit/6b6de48 - [@fearphage]: https://github.com/fearphage
.gitignore+1 −0 modified@@ -2,3 +2,4 @@ node_modules /test/keys /test/*.pem /test/encrypted-key-passphrase +package-lock.json
lib/sign-stream.js+6 −1 modified@@ -34,7 +34,12 @@ function jwsSign(opts) { } function SignStream(opts) { - var secret = opts.secret||opts.privateKey||opts.key; + var secret = opts.secret; + secret = secret == null ? opts.privateKey : secret; + secret = secret == null ? opts.key : secret; + if (/^hs/i.test(opts.header.alg) === true && secret == null) { + throw new TypeError('secret must be a string or buffer or a KeyObject') + } var secretStream = new DataStream(secret); this.readable = true; this.header = opts.header;
lib/verify-stream.js+6 −1 modified@@ -79,7 +79,12 @@ function jwsDecode(jwsSig, opts) { function VerifyStream(opts) { opts = opts || {}; - var secretOrKey = opts.secret||opts.publicKey||opts.key; + var secretOrKey = opts.secret; + secretOrKey = secretOrKey == null ? opts.publicKey : secretOrKey; + secretOrKey = secretOrKey == null ? opts.key : secretOrKey; + if (/^hs/i.test(opts.algorithm) === true && secretOrKey == null) { + throw new TypeError('secret must be a string or buffer or a KeyObject') + } var secretStream = new DataStream(secretOrKey); this.readable = true; this.algorithm = opts.algorithm;
package.json+2 −2 modified@@ -1,6 +1,6 @@ { "name": "jws", - "version": "4.0.0", + "version": "4.0.1", "description": "Implementation of JSON Web Signatures", "main": "index.js", "directories": { @@ -24,7 +24,7 @@ "readmeFilename": "readme.md", "gitHead": "c0f6b27bcea5a2ad2e304d91c2e842e4076a6b03", "dependencies": { - "jwa": "^2.0.0", + "jwa": "^2.0.1", "safe-buffer": "^5.0.1" }, "devDependencies": {
test/jws.test.js+116 −0 modified@@ -329,3 +329,119 @@ test('jws.isValid', function (t) { t.same(jws.isValid(valid), true); t.end(); }); + +test('Streaming sign: HMAC with empty secret buffer', function (t) { + const dataStream = readstream('data.txt'); + const secret = Buffer.alloc(0); + const sig = jws.createSign({ + header: { alg: 'HS256' }, + secret: secret + }); + dataStream.pipe(sig.payload); + sig.on('done', function (signature) { + t.ok(jws.verify(signature, 'HS256', secret), 'should verify'); + t.end(); + }).sign(); +}); + +test('Streaming sign: HMAC with empty secret string', function (t) { + const dataStream = readstream('data.txt'); + const secret = ''; + const sig = jws.createSign({ + header: { alg: 'HS256' }, + secret: secret + }); + dataStream.pipe(sig.payload); + sig.on('done', function (signature) { + t.ok(jws.verify(signature, 'HS256', secret), 'should verify'); + t.end(); + }).sign(); +}); + +test('Streaming sign: HMAC with undefined secret', function (t) { + try { + jws.createSign({ + header: { alg: 'HS256' }, + secret: undefined + }); + t.fail('should have errored'); + t.end(); + } catch (error) { + t.equal(error.name, 'TypeError'); + t.equal(error.message, 'secret must be a string or buffer or a KeyObject'); + t.end(); + } +}); + +test('Streaming sign: HMAC with null secret', function (t) { + try { + jws.createSign({ + header: { alg: 'HS256' }, + secret: null + }); + t.fail('should have errored'); + t.end(); + } catch (error) { + t.equal(error.name, 'TypeError'); + t.equal(error.message, 'secret must be a string or buffer or a KeyObject'); + t.end(); + } +}); + +test('Streaming verify: HMAC with empty secret buffer', function (t) { + const secret = Buffer.alloc(0); + jws.createVerify({ + signature: 'eyJhbGciOiJIUzI1NiJ9.b25lLCB0d28sIHRocmVlCg.V1oQ0aq6FgAoe7C2TORtYpQAbYzJoFNFZlJkTlF1e60', + algorithm: 'HS256', + secret: secret + }).on('done', function (valid, decoded) { + t.true(valid); + t.same(decoded.payload, readfile('data.txt')); + t.end(); + }).verify(); +}); + +test('Streaming verify: HMAC with empty secret string', function (t) { + const secret = ''; + jws.createVerify({ + signature: 'eyJhbGciOiJIUzI1NiJ9.b25lLCB0d28sIHRocmVlCg.V1oQ0aq6FgAoe7C2TORtYpQAbYzJoFNFZlJkTlF1e60', + algorithm: 'HS256', + secret: secret + }).on('done', function (valid, decoded) { + t.true(valid); + t.same(decoded.payload, readfile('data.txt')); + t.end(); + }).verify(); +}); + +test('Streaming verify: HMAC with undefined secret', function (t) { + try { + jws.createVerify({ + signature: 'eyJhbGciOiJIUzI1NiJ9.b25lLCB0d28sIHRocmVlCg.V1oQ0aq6FgAoe7C2TORtYpQAbYzJoFNFZlJkTlF1e60', + algorithm: 'HS256', + secret: undefined + }); + t.fail('should have errored'); + t.end(); + } catch (error) { + t.equal(error.name, 'TypeError'); + t.equal(error.message, 'secret must be a string or buffer or a KeyObject'); + t.end(); + } +}); + +test('Streaming verify: HMAC with null secret', function (t) { + try { + jws.createVerify({ + signature: 'eyJhbGciOiJIUzI1NiJ9.b25lLCB0d28sIHRocmVlCg.V1oQ0aq6FgAoe7C2TORtYpQAbYzJoFNFZlJkTlF1e60', + algorithm: 'HS256', + secret: null + }); + t.fail('should have errored'); + t.end(); + } catch (error) { + t.equal(error.name, 'TypeError'); + t.equal(error.message, 'secret must be a string or buffer or a KeyObject'); + t.end(); + } +});
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
7- github.com/advisories/GHSA-869p-cjfg-cm3xghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2025-65945ghsaADVISORY
- github.com/auth0/node-jws/commit/34c45b2c04434f925b638de6a061de9339c0ea2eghsax_refsource_MISCWEB
- github.com/auth0/node-jws/commit/4f6e73f24df42f07d632dec6431ade8eda8d11a6ghsaWEB
- github.com/auth0/node-jws/releases/tag/v3.2.3ghsaWEB
- github.com/auth0/node-jws/releases/tag/v4.0.1ghsaWEB
- github.com/auth0/node-jws/security/advisories/GHSA-869p-cjfg-cm3xghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.