From 0d8864492daddaac274135febfa373ab8b5c4b48 Mon Sep 17 00:00:00 2001 From: Hashim1999164 <64767361+Hashim1999164@users.noreply.github.com> Date: Fri, 18 Sep 2026 20:49:24 +0500 Subject: [PATCH 1/2] fix: parse HMAC string secrets without a public key attempt verify() always called createPublicKey first. For a normal HMAC string secret that throws, which dominated verify time. Try createSecretKey first for strings that are not PEM, SSH, or JWK, and keep PEM public keys on the public-key path so HS256 cannot treat them as HMAC secrets. --- test/issue_1046.tests.js | 23 +++++++++++++++++++++++ verify.js | 18 ++++++++++++++++-- 2 files changed, 39 insertions(+), 2 deletions(-) create mode 100644 test/issue_1046.tests.js diff --git a/test/issue_1046.tests.js b/test/issue_1046.tests.js new file mode 100644 index 00000000..1546f5fb --- /dev/null +++ b/test/issue_1046.tests.js @@ -0,0 +1,23 @@ +const jwt = require('../index'); +const crypto = require('crypto'); +const assert = require('chai').assert; +const expect = require('chai').expect; +const JsonWebTokenError = require('../lib/JsonWebTokenError'); + +describe('issue 1046', function () { + it('verifies HS256 tokens signed with a string secret', function () { + const secret = 'a-shared-secret-of-reasonable-length'; + const token = jwt.sign({ sub: 'u' }, secret, { algorithm: 'HS256' }); + const decoded = jwt.verify(token, secret, { algorithms: ['HS256'] }); + assert.equal(decoded.sub, 'u'); + }); + + it('still rejects HMAC verification with a PEM public key', function () { + const { publicKey } = crypto.generateKeyPairSync('rsa', { modulusLength: 2048 }); + const pem = publicKey.export({ type: 'spki', format: 'pem' }); + const maliciousToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InJzYUtleUlkIn0.eyJmb28iOiJiYXIiLCJpYXQiOjE2NTk1MTA2MDh9.cOcHI1TXPbxTMlyVTfjArSWskrmezbrG8iR7uJHwtrQ'; + + expect(() => jwt.verify(maliciousToken, pem, { algorithms: ['RS256', 'HS256'] })) + .to.throw(JsonWebTokenError, 'must be a symmetric key'); + }); +}); diff --git a/verify.js b/verify.js index cdbfdc45..f905333f 100644 --- a/verify.js +++ b/verify.js @@ -18,6 +18,15 @@ if (PS_SUPPORTED) { RSA_KEY_ALGS.splice(RSA_KEY_ALGS.length, 0, 'PS256', 'PS384', 'PS512'); } +// PEM/SSH/JWK material must still go through createPublicKey first so an HS* +// token cannot treat an RSA public key as an HMAC secret. +function looksLikeAsymmetricKey(key) { + if (typeof key !== 'string') { + return true; + } + return /^\s*(-----BEGIN |ssh-|\{)/.test(key); +} + module.exports = function (jwtString, secretOrPublicKey, options, callback) { if ((typeof options === 'function') && !callback) { callback = options; @@ -118,11 +127,16 @@ module.exports = function (jwtString, secretOrPublicKey, options, callback) { } if (secretOrPublicKey != null && !(secretOrPublicKey instanceof KeyObject)) { + const trySecretFirst = !looksLikeAsymmetricKey(secretOrPublicKey); try { - secretOrPublicKey = createPublicKey(secretOrPublicKey); + secretOrPublicKey = trySecretFirst + ? createSecretKey(typeof secretOrPublicKey === 'string' ? Buffer.from(secretOrPublicKey) : secretOrPublicKey) + : createPublicKey(secretOrPublicKey); } catch (_) { try { - secretOrPublicKey = createSecretKey(typeof secretOrPublicKey === 'string' ? Buffer.from(secretOrPublicKey) : secretOrPublicKey); + secretOrPublicKey = trySecretFirst + ? createPublicKey(secretOrPublicKey) + : createSecretKey(typeof secretOrPublicKey === 'string' ? Buffer.from(secretOrPublicKey) : secretOrPublicKey); } catch (_) { return done(new JsonWebTokenError('secretOrPublicKey is not valid key material')) } From 4adfb049f7f2f17148ff4fe0f84f9ab0f643f1d1 Mon Sep 17 00:00:00 2001 From: Hashim1999164 <64767361+Hashim1999164@users.noreply.github.com> Date: Wed, 30 Sep 2026 22:08:46 +0500 Subject: [PATCH 2/2] fix: detect a PEM header anywhere in the key string --- test/issue_1046.tests.js | 24 ++++++++++++++++++++++++ verify.js | 3 ++- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/test/issue_1046.tests.js b/test/issue_1046.tests.js index 1546f5fb..f93f961e 100644 --- a/test/issue_1046.tests.js +++ b/test/issue_1046.tests.js @@ -20,4 +20,28 @@ describe('issue 1046', function () { expect(() => jwt.verify(maliciousToken, pem, { algorithms: ['RS256', 'HS256'] })) .to.throw(JsonWebTokenError, 'must be a symmetric key'); }); + + it('rejects an HS256 token when the PEM public key has text before the header', function () { + const { publicKey } = crypto.generateKeyPairSync('rsa', { modulusLength: 2048 }); + const key = 'public key for service X\n' + publicKey.export({ type: 'spki', format: 'pem' }); + const b64u = (value) => Buffer.from(JSON.stringify(value)).toString('base64url'); + const data = `${b64u({ alg: 'HS256', typ: 'JWT' })}.${b64u({ sub: 'attacker' })}`; + const token = `${data}.${crypto.createHmac('sha256', key).update(data).digest('base64url')}`; + + expect(() => jwt.verify(token, key)).to.throw(JsonWebTokenError, 'invalid algorithm'); + }); + + it('verifies a string secret the same as its KeyObject', function () { + const secret = 'a-shared-secret-of-reasonable-length'; + const token = jwt.sign({ sub: 'u' }, secret, { algorithm: 'HS256' }); + const keyObject = crypto.createSecretKey(Buffer.from(secret)); + const fromString = jwt.verify(token, secret); + const fromKey = jwt.verify(token, keyObject); + assert.deepEqual(fromString, fromKey); + }); + + it('still rejects a wrong string secret', function () { + const token = jwt.sign({ sub: 'u' }, 'correct-secret-value', { algorithm: 'HS256' }); + expect(() => jwt.verify(token, 'wrong-secret-value')).to.throw(); + }); }); diff --git a/verify.js b/verify.js index f905333f..4517086c 100644 --- a/verify.js +++ b/verify.js @@ -24,7 +24,8 @@ function looksLikeAsymmetricKey(key) { if (typeof key !== 'string') { return true; } - return /^\s*(-----BEGIN |ssh-|\{)/.test(key); + // OpenSSL accepts text before a PEM header, so BEGIN must match anywhere. + return key.indexOf('-----BEGIN') !== -1 || /^\s*(ssh-|\{)/.test(key); } module.exports = function (jwtString, secretOrPublicKey, options, callback) {