VYPR
High severityNVD Advisory· Published Dec 4, 2025· Updated Dec 5, 2025

auth0/node-jws improper HMAC signature verification vulnerability

CVE-2025-65945

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.

PackageAffected versionsPatched versions
jwsnpm
< 3.2.33.2.3
jwsnpm
>= 4.0.0, < 4.0.14.0.1

Affected products

2
  • Auth0/node-jwsllm-create
    Range: <3.2.3 || >=4.0.0 <4.0.1
  • auth0/node-jwsv5
    Range: < 3.2.3

Patches

2
4f6e73f24df4

Merge commit from fork

https://github.com/auth0/node-jwsanicho123Dec 4, 2025via ghsa
6 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();
    +  }
    +});
    
34c45b2c0443

Merge commit from fork

https://github.com/auth0/node-jwsanicho123Dec 4, 2025via ghsa
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

News mentions

0

No linked articles in our index yet.