JWT で CRSF 対策を実装する
Rails などのフレームワークの CSRF 対策の実装を使わず、自前で CSRF 対策を行ってみる。
そもそも CSRF とは?
リクエストが「なぜ起こったか?」に関わらず、ブラウザが自動的に Cookie などのセッション情報を送信することを利用した攻撃方法。
例えば、ある人がサイト A にログインしているとする。この人がサイト B を訪問したとき、サイト B が iframe を経由してサイト A を埋め込んでいた場合、サイト A はログインされた状態で読み込まれる。 この性質を利用すれば、サイト B は、サイト A の GET リクエストでできるアクションを強制的に起こすことができる。
GET リクエストだけが危ないのかといえばそうではなく、iframe を target とした form を利用して POST リクエストも起こすことができる。
これを利用すれば、ログイン状態でデータ全削除機能の POST を叩かせる、といったことができてしまう。
対策方法
自サイトの form から自発的に送信されたリクエストであることを検証できれば、上記のような攻撃を防止することができる。
ではどうするか。よく使われるのはワンタイムトークンを form に input type=hidden などで埋め込んで一緒に送ってもらう方法。 しかし、ワンタイムトークンを使うとセッション情報を保持しなければならず、面倒臭い。 ここで、JWT を使うことでステートレスに CSRF 対策を行うことができる。
そもそも、自サイトを経由して送られたリクエストであることが検証できればよいのだから、ワンタイムトークンを使うまでもない。
具体的には sub
を post
などとし、aud
には最初のアクセス時に Cookie に保持させたランダム文字列を使い、サーバのもつ単一の秘密鍵で署名する。
この JWT を form の hidden に挿入しておき、そこから送信された POST リクエストを次のように検証する。
- JWT の検証が成功すること
sub
がpost
であること。aud
の値が、前述の Cookie に保持させた文字列(さっきaud
に入れたやつ)と等しいこと。
JWT にはサーバのみにある秘密鍵を使った署名が付与されているので、勝手につくることや、改ざんはできない。もしそのようなトークンが送信された場合は検証に失敗する。勝手に値をいれて作ったり、改ざんしたりができないから、aud
の値を自由に弄ることはできない。
このようにすると、aud
がユーザ毎(ブラウザ毎)に違うからユーザ毎に違うトークンを使うことになり、トークン再利用攻撃も防止されている。
ソース
面倒臭いので割愛
まとめ
このようなセキュリティ関係は考え出すと面倒臭い。特にこの CSRF 脆弱性は単純な割にはかなり致命的な攻撃ができてしまう。 Ruby の SInatra や Go の http/net などの軽量フレームワークを使う場合は、このような実装も考えていくことになる。
ほかによくある攻撃は SQL インジェクション、XSS など。 これらはデータベースドライバや言語レベルで対策が可能な場合が多く、今回の CSRF の実装が最も面倒臭い。