JWT 脆弱性
で検索すると 【翻訳】JSON Web Tokenライブラリの危機的な脆弱性 って記事(4年ぐらい前の記事)が出てきて読んでいたのですが、よくわからなかったので手を動かしながら調べてみました。
※ 翻訳の元記事はこちら
node-jsonwebtoken, pyjwt, namshi/jose, php-jwt, jsjwtを非対称の鍵(RS256, RS384, RS512, ES256, ES384, ES512)で使っている場合、最新のバージョンに更新してください。
とのことなので、バージョン更新で直ったようなので修正を読んでいきます。
また、 js を利用して開発する機会が多いので node-jsonwebtoken の修正を確認してみます。
確認したコードは下記にあります。
https://github.com/OshiroSeiya/jsonwebtoken-audit-check
脆弱性の概要を知る
none アルゴリズムの話は飛ばします。元記事の内容を見たほうが良いです。
RS256 アルゴリズムを利用して JWT を発行している場合に攻撃者が HS256 アルゴリズムを利用して発行した JWT のチェックが通ってしまう問題があります。
RS256 は JWT を発行するのは秘密鍵を用いて発行し、チェックには公開鍵を利用します。
一報 HS256 は JWT を発行する時とチェックに利用する鍵が同じになります。
認証側のチェックが下記のようになっていた場合
※ TOKEN: 発行されたJWT
※ KEY: はチェックの際に利用する鍵です
jwt.verify(TOKEN, KEY)
利用するアルゴリズムが TOKEN の方に入っているためチェックの際に RS256 で検証するべきなのか HS256 で検証するべきなのか判断します。
RS256 でTOKENを発行する仕組みになっている場合、攻撃者が HS256 のアルゴリズムで公開鍵(利用用途的に公開されているので誰でも取得可能)を用いて TOKEN を発行するとチェックする際に HS256 を用いてチェックされ KEY は同じ公開鍵を利用するためチェックが通ってしまうことになり、改ざんが可能になるということみたいです。
対応されたライブラリのPRなどを見る
修正は下記のようです。
https://github.com/auth0/node-jsonwebtoken/pull/69
https://github.com/auth0/node-jsonwebtoken/commit/7017e74db9b194448ff488b3e16468ada60c4ee5
https://github.com/auth0/node-jsonwebtoken/pull/71
この修正が入る以前と以後のライブラリを利用して挙動の確認とどういう対策が行われたのかを確認してきます。
修正前: 4.1.0 (https://github.com/auth0/node-jsonwebtoken/tree/b69d441c6e5e4b2efaafde682b4b9670ac3bcb51)
修正後: 4.2.2 (https://github.com/auth0/node-jsonwebtoken/tree/e46ca6634447cf6a5b7f08298aa2f2450b8df704)
修正前のバージョンで問題が起きることを確認する
4.2.0で追加されたコードは下記でした。
https://github.com/auth0/node-jsonwebtoken/pull/69/files
if (!options.algorithms) {
options.algorithms = ~secretOrPublicKey.toString().indexOf('BEGIN CERTIFICATE') ?
[ 'RS256','RS384','RS512','ES256','ES384','ES512' ] :
[ 'HS256','HS384','HS512' ];
}
var header = jws.decode(jwtString).header;
if (!~options.algorithms.indexOf(header.alg)) {
return done(new JsonWebTokenError('invalid signature'));
}
要するに jwt.verify メソッドを利用する時の secretOrPublicKey
の値に BEGIN CERTIFICATE
が含まれる場合は [ 'RS256','RS384','RS512','ES256','ES384','ES512' ]
のみ利用できるようになるということですね。
※ 4.2.1で BEGIN PUBLIC KEY
が含まれる場合の処理も追加されていますがやりたいことは同じなので飛ばします。
※ 4.2.2で BEGIN RSA PUBLIC KEY
が含まれる場合の処理が追加されていますがやりたいことは同じなので飛ばします。
4.2.0で追加されたテストを見ます。
https://github.com/auth0/node-jsonwebtoken/pull/69/files#diff-f676fb748f383a87d0f55bec4f266023
テストコードを書きながらためしたほうが僕は理解しやすいので下記のようにコードを書きました。
※ 公開鍵と秘密鍵はテストで利用されていたものと同じものを利用しています。
https://github.com/OshiroSeiya/jsonwebtoken-audit-check/blob/master/src/4.1.0/index.test.js
const fs = require('fs');
const path = require('path');
const jwt = require('jsonwebtoken');
// 公開鍵
const PUB = fs.readFileSync(path.join(__dirname, 'pub.pem'), 'utf8');
// 秘密鍵
const PRIV = fs.readFileSync(path.join(__dirname, 'priv.pem'), 'utf8');
// payload
const payload = {
foo: "bar"
};
test('サーバー側で署名した TOKEN がチェックを通過し payload が取得できる', () => {
// サーバー側で秘密鍵を利用し、アルゴリズムを RS256 で署名した TOKEN
const RS256_TOKEN = jwt.sign(payload, PRIV, {algorithm: 'RS256'});
expect(jwt.verify(RS256_TOKEN, PUB)).toEqual(payload);
});
test('攻撃者が署名した TOKEN がチェックを通過し payload が取得できる', () => {
// 攻撃者がRS256の公開鍵を利用し、アルゴリズムを HS256 で署名したTOKEN
// ※HS256はdefaultのアルゴリズムですがわかりやすいように設定
const HS256_TOKEN = jwt.sign(payload, PUB, {algorithm: 'HS256'});
expect(jwt.verify(HS256_TOKEN, PUB)).toEqual(payload);
});
PASS ./index.test.js
√ サーバー側で署名した TOKEN がチェックを通過し payload が取得できる (9ms)
√ 攻撃者が署名した TOKEN がチェックを通過し payload が取得できる (1ms)
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 4.022s
Ran all test suites.
しっかりと攻撃者が署名した TOKEN がチェックを通過し payload が取得することができています。
修正後のバージョンで問題が起きないことを確認する
先程作ったテストを 4.2.2 に node-jsonwebtoken をアップデートしてテストを実行してみます。
FAIL ./index.test.js
√ サーバー側で署名した TOKEN がチェックを通過し payload が取得できる (9ms)
× 攻撃者が署名した TOKEN がチェックを通過し payload が取得できる (4ms)
● 攻撃者が署名した TOKEN がチェックを通過し payload が取得できる
JsonWebTokenError: invalid signature
24 | const HS256_TOKEN = jwt.sign(payload, PUB, {algorithm: 'HS256'});
25 |
> 26 | expect(jwt.verify(HS256_TOKEN, PUB)).toEqual(payload);
| ^
27 | });
28 |
at Object.<anonymous>.module.exports.verify (node_modules/jsonwebtoken/index.js:141:17)
at Object.verify (index.test.js:26:14)
Test Suites: 1 failed, 1 total
Tests: 1 failed, 1 passed, 2 total
Snapshots: 0 total
Time: 3.199s
Ran all test suites.
npm ERR! Test failed. See above for more details.
結果、攻撃者が署名した TOKEN はしっかりとエラーになっていることが確認できました。
まとめ
記載されていた通り、 node-jsonwebtoken
の新しいバージョンであれば問題は起きなくなっていました。
ライブラリで対応(利用できるアルゴリズムを絞る)されてはいますが、仕様的には今後も起きる可能性はなくはないと思われます。
ライブラリを利用するときは対策されているか確認してみると良さそうです。