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] 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')) }