Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions test/issue_1046.tests.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
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');
});

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();
});
});
19 changes: 17 additions & 2 deletions verify.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,16 @@ 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;
}
// 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) {
if ((typeof options === 'function') && !callback) {
callback = options;
Expand Down Expand Up @@ -118,11 +128,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'))
}
Expand Down