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