👹

JWTのalg=noneによる署名検証回避はどうして起こるのか

2021/09/08に公開
2

おはようございます。ritouです。

なんの話?

これの話です。

RFC 8725 JSON Web Token Best Current Practices をざっくり解説する - Qiita

攻撃者が none に書き換え、検証側がそれを信用して署名検証をスキップ : ライブラリが JWT Header の alg の値を信用して署名検証をスキップしてしまうお話です

攻撃者が RS256 を HS256 に書き換え、検証側は RSA 公開鍵を HMAC の共有鍵として署名検証 : こちらも JWT Header の alg の値を信用し、署名検証用の関数の引数として指定したRSA公開鍵を共有鍵として扱ってしまうお話です

どっちも「おいおい冗談だろ」みたいなお話に見えますが、そういう実装もあるのが事実なんですね。

どうしてこうなった?

署名検証ロジックが

  1. JWT文字列と鍵情報をパラメータに受ける
  2. JWTのHeaerをデコード
  3. "Headerで指定されているアルゴリズムと鍵で" 署名を検証

となっている場合にこの問題が起こり得ます。

  1. JWT文字列と鍵情報をパラメータに受ける
  2. JWTのHeaerをデコード
  3. "鍵に紐づけられたアルゴリズムと一致することを確認した上で" 署名を検証

となっていたら大丈夫です。

=== 2021/9/9追記 ===

コメントいただきました。

JWKではalgはオプションですし、実際、algを指定していない jwksエンドポイントは普通にあります。しかも、kty と crv でalgが同定できるわけでもない kty=RSAなやつまで。
この方向性での解決策なら、
"JWTのヘッダに指定されたalgが属するktyと、鍵に紐づけられたktyが一致することを確認した上で"
じゃないですかね。ktyはRFC的にもMUSTですし、これならalg=noneも、HS<=>RSも回避できます。

ご指摘の通り、RFCの整合性を取りつつライブラリが実装するのであればこの辺りの実装が落とし所になりそうです。

===

また、前者でありつつも利用できるalgの値を指定して絞れるようにしている ライブラリは問題が出ないでしょう。

少し例示します。
alg=none な JWT の値は A.5. Example Unsecured JWS @ RFC7515 JSON Web Signature (JWS) にあります。

eyJhbGciOiJub25lIn0.eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ.

# 改行入れると
eyJhbGciOiJub25lIn0
.
eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ
.

# Base64 URLデコードすると
{
  "alg": "none"
}
.
{
  "iss": "joe",
  "exp": 1300819380,
  "http://example.com/is_root": true
}
.

これをElixir/ErlangのJWTライブラリであるJOSEで署名検証してみます。

# JWT文字列
iex(1)> jwt = "eyJhbGciOiJub25lIn0.eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ."
"eyJhbGciOiJub25lIn0.eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ."

# HS256とかで使いそうなバイナリ形式の署名生成/検証用の鍵
iex(2)> jwk = JOSE.JWK.from(%{"k" => "qUg4Yw", "kty" => "oct"})        
%JOSE.JWK{
  fields: %{},
  keys: :undefined,
  kty: {:jose_jwk_kty_oct, <<169, 72, 56, 99>>}
}

# alg=noneを許容するおまじない
iex(3)> JOSE.unsecured_signing(true)
:ok

# verify/2 を用いて alg=none な JWT をバイナリ形式の鍵で検証...成功!?!?
iex(4)> JOSE.JWT.verify(jwk, jwt)  
{true,
 %JOSE.JWT{
   fields: %{
     "exp" => 1300819380,
     "http://example.com/is_root" => true,
     "iss" => "joe"
   }
 }, %JOSE.JWS{alg: {:jose_jws_alg_none, :none}, b64: :undefined, fields: %{}}}
 
# verify_strict/3 で許容する alg に "none" の値を指定するのと同じ挙動
iex(5)> JOSE.JWT.verify_strict(jwk, ["none"], jwt)  
{true,
 %JOSE.JWT{
   fields: %{
     "exp" => 1300819380,
     "http://example.com/is_root" => true,
     "iss" => "joe"
   }
 }, %JOSE.JWS{alg: {:jose_jws_alg_none, :none}, b64: :undefined, fields: %{}}}

# alg=noneを許容しないおまじない(デフォルト)
iex(6)> JOSE.unsecured_signing(false)               
:ok

# verify/2 でも通らなくなるし
iex(7)> JOSE.JWT.verify(jwk, jwt)                   
{false,
 %JOSE.JWT{
   fields: %{
     "exp" => 1300819380,
     "http://example.com/is_root" => true,
     "iss" => "joe"
   }
 }, %JOSE.JWS{alg: {:jose_jws_alg_none, :none}, b64: :undefined, fields: %{}}}
 
# verify_strict/3 でも同様
iex(8)> JOSE.JWT.verify_strict(jwk, ["none"], jwt)
{false,
 %JOSE.JWT{
   fields: %{
     "exp" => 1300819380,
     "http://example.com/is_root" => true,
     "iss" => "joe"
   }
 }, %JOSE.JWS{alg: {:jose_jws_alg_none, :none}, b64: :undefined, fields: %{}}}

ErlangのJWTライブラリの実装は前者の"Headerで指定されているアルゴリズムと鍵で"署名検証を行うものになっています。

そのため、検証用の鍵とJWTを引数として受け取る verify/2 では Headerに "alg=none" が指定されているとそちらを見て署名検証がOKとなってしまうわけです。(4)

verify_strict/3では引数に"許容するalg"のリストを受け取り、それに含まれていたらそのアルゴリズムで検証しましょうという実装になっているため、noneの値を明示的に指定すると署名検証がOKとなります。(5)

特に verify/2 は気軽に使いたい開発者もいると思うので、処理全体の中でalg=noneを許容するかどうかを unsecured_signing/1 という関数で指定できるようになっています。(3), (6)

この値がfalse(デフォルト)であれば、verify/2, verify_strict/3それぞれでalg=noneによる署名検証が通らなくなります。

JWTの署名検証処理なんてのはライブラリに任せておくのが鉄則と言っても良い状況だと思っていますが、今一度自分の使っているライブラリがしっかりとこの辺りに対応できているかを見直してみるのも良いでしょう。

もしライブラリレベルでalg=noneで署名検証をスキップできる可能性がある場合、その署名検証関数を呼び出すロジックを工夫したり安全に利用できるようにラップしたライブラリを書くなどする必要があるでしょう。

もっとJWTについて知りたい?

個人的には、

  • 鍵と許容するアルゴリズムを1:1(もしくは1:n)で管理
  • さらにそれをkidで識別可能にする
  • 複数の鍵を署名検証の関数で指定できる

みたいな実装が良いのではないかなと思っており、実際にそういう挙動になるようなラッパーライブラリを実装して利用しています。kitten_blue

この辺りの考えについてはこれまでブログ記事を書いているので参考にどうぞ。

ではまた!

Discussion

dajiajidajiaji

"鍵に紐づけられたアルゴリズムと一致することを確認した上で" 署名を検証

こういう実装ってありえるんですかね・・。

JWKではalgはオプションですし、実際、algを指定していない jwksエンドポイントは普通にあります。しかも、kty と crv でalgが同定できるわけでもない kty=RSAなやつまで。

この方向性での解決策なら、

"JWTのヘッダに指定されたalgが属するktyと、鍵に紐づけられたktyが一致することを確認した上で"

じゃないですかね。ktyはRFC的にもMUSTですし、これならalg=noneも、HS<=>RSも回避できます。

また、前者でありつつも利用できるalgの値を指定して絞れるようにしている ライブラリは問題が出ないでしょう。

(是非は別にして)これが一般的かなとは思いますね。ここに異論はないです。

ritouritou

"JWTのヘッダに指定されたalgが属するktyと、鍵に紐づけられたktyが一致することを確認した上で"

ご指摘のとおり、RFCの整合性を取った上でのライブラリ実装としてはこのあたりが落とし所になりそうですね。

algのリストを指定して絞るのはたしか2013年ぐらいにこの攻撃というか問題が出たときに各言語のライブラリで一斉に対応された対策だったと記憶しています。