JWT(JSON Web Token)でCSRF脆弱性を回避できるワケを調べてみた話


はじめに

こんにちは。流通事業部、新卒2年目の@mejilebenです。ガチアサリ難しすぎる。。。
※本記事はLIFULL Advent Calendar 2017 20日目の記事です。

背景

JWT(JSON Web Token)という技術があるのですが、この技術を使うとCSRF脆弱性の対策にもなるということを知って、いったいどういう理屈なのか調べてみました。
色々な意味でツッコミどころ満載の記事になっていますが、お手柔らかにコメントいただけるとうれしいです。

この記事で言いたいこと

  • JWTは改ざんを検知できる等の便利な仕様であることから、Webアプリケーションにおいて認証や認可の用途で使われている
  • CSRFは悪意のある第三者による偽造されたリクエストも本物とみなして処理をしてしまう脆弱性
  • ユーザーからのリクエスト時はJWTをAuthorizationヘッダに載せることで、CSRF脆弱性を防ぎながらJWTの恩恵にあやかることができる

JWTってどんな技術?

概要

「じぇーだぶりゅーてぃー」ではなく、「じょっと」と読みます。
凄いざっくりいうと認証だったり認可の文脈で使われる技術で、ユーザーに関する情報などを含めたJSONを秘密鍵で電子署名してサーバー・クライアント間でやり取りすることで認証や認可をセキュアに実現します。
暗号化と電子署名の違いはこの記事が分かりやすかった

JWTについては既にQiitaで説明されている記事がいくつかありました。

例えばこの記事では下記のような説明がされています。

電子署名付きの URL-safe(URLとして利用出来る文字だけ構成される)な JSONのことです。
電子署名により、JSON の改ざんをチェックできるようになっています。

悪意のある第三者によってクライアント側でJWTがこっそり書き換えられた場合でも、サーバー側で復号するときに失敗するようになっており、認証もしくは認可失敗として処理を止めることができます。

これって当たり前のようで中々今まで実現できなかったことで、ステートレスな挙動が基本のWebで無理やりステートレスにしようとしていた既存の認証系の実装をシンプルにできそうです。

RFC7519を読んでみても良いのですが、JWSとかJWE、claimといった独特な単語が次々登場するため、余裕があれば読めばいいと思います笑
このあたりのサイトが詳しく書いていただいています。

JWTの例

実際見てみましょう。たとえば下記の文字列がJWTです。

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwidXNlcl9pZCI6Im1lamlsZWJlbiJ9.D9753NemToDglTjlquJTA0avNXg7pkxZFjla3DGF33E

これを復号化すると、下記のようなJSONが得られます。

{
  "sub": "1234567890",
  "user_id": "mejileben"
}

URL-safeとは・・・?

本筋からずれるのですが、URL-safeがURLで利用できる文字を意味するとして、

  • じゃあ具体的にURLで利用できる文字とそうでない文字ってなんなんだ?
  • どうしてURL-safeである必要があるのか?

という2つの疑問が出てきたのでちょっと補足します。

前者に関して。
「url reserved characters rfc」で検索するとRFC2396RFC3986の2つが出てきて、しかも全く同じ趣旨のようでなおさら混乱してしまいました。

ただ、3986のほうが時期的には新しいので、こちらに従うのが安全なのでしょう(古いライブラリならどちらに従っているか分からないので注意する必要がありそうですが)。

  unreserved  = ALPHA / DIGIT / "-" / "." / "_" / "~"

とあるので、英字、数字、ハイフン、ドット、アンダーバー、チルダがURLで使える文字ということですね。

後者に関して。
JWTの有力な使いみちとして、「このURLを24時間以内にクリックしたらユーザー登録完了だよ!メール」があります。

この場合は、例えばJSONにユーザーIDとメールアドレス、そして有効期限(24時間後)を含ませて電子署名してJWTを作成し、作ったJWTをURLの末尾などにくっつけることでそのURLを作る方法が使えます。

あの手のメールで来るURLって見るからに無茶苦茶な文字列で作られてますよね。あれをJWTにすることで、サーバー側で「どのメールアドレスにどの登録完了URL送ったっけ?」と記憶しておかなくても、飛んできたリクエストのURLから末尾のJWTを取り出して復号化してメルアドとユーザーID取り出せばいいので、キャッシュとか使わなくて良くなる感じです。

という使いみちもあるよねってなると、JWTの中にURLで使えない文字を入れていたらダメなわけです。

CSRF脆弱性って何?

さて、次はCSRFの話をします。
CSRFはWebアプリケーションにおける脆弱性の一種です。
脆弱性というのは悪用できるバグのことをいいます。
そして、CSRFは「しーさーふ」と読まれることがあります。

この脆弱性の恐ろしいところは、いわゆる「成りすまし」に近いことができることです。
数年前に、横浜市のHPに小学校の爆破予告が書き込まれ、犯人が一時誤認逮捕されたニュースが有りました。
https://matome.naver.jp/odai/2135078276846688301)
どうして誤認逮捕されたかというと、CSRF脆弱性を突いて他人のパソコンから爆破予告を書き込ませることができたからです。

CSRF脆弱性の例

CSRF脆弱性のある掲示板サイト「www.csrf脆弱性ありますよー.com」があったとして、攻撃者はこんなサイトを作って適当な偽物掲示板サイト「www.このページ見たら成り済まされますよ−.com」に配置しておきます。

「www.このページ見たら成り済まされますよ−.com」
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>CSRF test</title>
</head>
<body>
<div>
    <form action='http://www.csrf脆弱性ありますよー.com' method='POST'>
        <input type="text" name="username" value="LIFULL善次郎"/>
        <input type="text" name="text" value="明日、お一人様一箱限定のティッシュを五箱買います">
    </form> 
</div>
<script>
    document.forms[0].submit();
</script>
</body>
</html>

鬼のように適当なソースで恐縮ですが、このページは開いた瞬間にJavaScriptによってformの内容が「www.csrf脆弱性ありますよー.com」にPOSTされます。
そして、formの中には「明日、お一人様一箱限定のティッシュを五箱買います」という、絶対にやってはいけない犯行予告が書いてあります。

次に、「www.このページ見たら成り済まされますよ−.com」をLIFULL善次郎さんに開いてもらいます。
(※LIFULL善次郎さんは「www.csrf脆弱性ありますよー.com」にログイン済みのユーザーです)

「www.csrf脆弱性ありますよー.com」は、cookieにセッションIDを格納しており、セッションIDだけで誰からPOSTされた文章か判断しています。
すると、このようなサイトの場合、結局LIFULL善次郎さんのブラウザから「www.csrf脆弱性ありますよー.com」にPOSTされることになるのでCookieがサーバーに持って行かれることになります。
サーバーにはちゃんとセッションIDが届いたので、認証OKとしてティッシュ五箱買う宣言を投稿できてしまいました。

結果、善次郎さんはちゃんと一箱しかティッシュを買わない人にも関わらず、五箱買う旨の文章がサーバーに投稿されてしまい、近所の方々から白い目で見られることになってしまいました。。。

簡単にいえば、ただの認証に加えて、サーバー側が「自分で表示したページからリクエストが飛んでいること」を把握する手段がないといけません。
このケースは、善次郎さんが悪いのではなくて、リクエストがどこから来たか判別できていないサイト側に問題があると考えるべきです(もちろん善次郎さんも怪しげなサイトを踏みに行かなければ良いのですが)。

補足
CSRFはCross Site Request Forgeryの略で、Forgeryは「偽造」という意味です。こちらも覚えると意味がわかりやすいかもしれません。

CSRF脆弱性対策としてのJWTの利用

CSRF対策は必ずしもJWTだけではありません。
少々難しいですがこのサイトに割と詳細に書いてあります。

ただ、ここではJWTに絞って話をします。

サーバーはログイン時にJWTを渡す

まず、上記のように掲示板といったサイトでは、まずログインをして貰う必要があり、それによりユーザー認証をしています。
その際に、認証に成功しログインしたユーザーにサーバーからJWTを渡してあげます。

これで、JWTを持っているユーザーはログイン済みのユーザーであることになります。

クライアントはリクエスト時にJWTをサーバーに渡す

ユーザーから掲示板への投稿といった、アカウントに紐付いたリクエストをサーバーに飛ばす時は、必ずJWTをサーバーに渡すようにします。
そして、サーバー側でJWTを復号し、復号が成功して、かつJSONから取り出したuser_idをチェックするなどして投稿の成否を判断します。

Authorizationヘッダを用いてJWTを渡す

このとき、JWTをサーバーに渡す時はリクエストのAuthorizationヘッダにJWTを載せてあげることで、CSRF脆弱性を防ぐことができます。
Cookieに保存しちゃっていると、結局善次郎さんのように罠サイトを踏んだときにJWTがサーバーに渡ってしまうので意味が無いのです。

罠サイトからは下記の理由によりAuthorizationヘッダを変更することができませんので、ヘッダにJWTが載っている = 自分のサイトから来たリクエストであると言えます。

  • form要素からAuthorizationヘッダを変更することはできない
  • JavaScriptでAjax通信を使えばヘッダを変更できるが、Ajax通信はクロスドメイン(つまり「www.このページ見たら成り済ませますよ−.com」から「www.csrf脆弱性ありますよー.com」にリクエストを飛ばす用途)で利用できない仕様になっている

また、どうして数ある中からAuthorizationヘッダなのかは、こちらの記事が分かりやすかったです。

自サイトのドメインからならJavaScriptでヘッダを変更できるため、jQueryの$.ajaxでヘッダを変更したりaxiosでもヘッダを変更することができます

まとめ

  • JWTは改ざんを検知できることから、Webアプリケーションにおいて認証や認可の用途で使われている
  • CSRFは悪意のある第三者がリクエストを偽造できてしまう
  • ユーザーからのリクエスト時はJWTをAuthorizationヘッダに載せることで、CSRF脆弱性を防ぎながらJWTの恩恵にあやかることができる

調べてみるとJWTだからCSRFを解決できる、というよりは数あるCSRFを解決する手法の中に、一手段としてJWTを使う手法があるといった感じでした。

一気に書いたのでどことなく中途半端な感じがしますが、読んだ方に少しでもためになる気づきを与えられたら何よりです。
例えばCSRF脆弱性を防いだとしてもXSS脆弱性が残っていたら全部水の泡になったりすると思うので、セキュリティはまだまだ奥が深いし勉強することだらけやなあと思います。

最後に

株式会社LIFULLでは一緒にビジョンを達成する仲間を募集しております!(ちゃっかり)
http://recruit.lifull.com/
オシャレなサイトなので、見るだけでもぜひ!(ちゃっかり)