親バカエンジニアのナレッジ帳

webのエンジニアをやっており、日頃の開発で詰まったことや書き残しておきたいことを載せています。

SPAサイトのCSRF対策


CSRF対策について

CSRFとは?

このページに来られた方ならもう理解している方も多いとは思いますが、CSRFについて簡単に説明します。
CSRFはリクエスト強要(CSRF:Cross-site Request Forgery)という意味で、クロスサイト(Cross-site)の名の通り、正規のサイトとは別のサイトからリクエストを送らせる偽造工作です。

Webでサーバサイドの開発をしている方ならご存知だとは思いますが、例えばフォームのPOST処理は必要な項目やURLさえわかっていればどこからでもリクエストすることは可能ですよね。(リクエスト先が受け付けてくれるかは別として)

よって、例えば攻撃者が攻撃用サイトを用意し、その中にリンクを設置しておくとします。
何も知らないユーザAがそれをクリックすると、勝手に別なWebアプリケーションのPOST処理が走るように仕込んでおけば、ユーザAがあたかもPOST処理を実行したかのように装ったリクエストを送信できるのです。
以下のサイトの図がわかりやすいです。

クロスサイトリクエストフォージェリ(CSRF) | トレンドマイクロ

CSRFによる被害

ログインを要しないサイトの処理であればユーザAが直接的な被害を被ることはありません。
ただし、掲示板に殺害予告など悪意のある書き込みをするような処理が送られた場合、場合によってはユーザAが誤認逮捕されてしまう恐れなどがあります。
ユーザA自身は何もしていなくても、ユーザAのネットワークのIPアドレスからリクエストが送信されていれば、ユーザAが行ったものと認識されてしまうためです。
実際にそのようなケースでの逮捕されてしまう人は後を絶ちません。

ログインを要するサイトであれば直接的な被害を被ることがあります。
例えばブログサービスなどで、自分は何もしていなくても勝手に投稿を書き込まれてしまうケースなどがあります。
一時期mixiで問題になった「はまちちゃん」などは有名ですね。

www.itmedia.co.jp

また、決済処理が備わっているWebアプリケーションのPOST処理であれば、勝手にものを購入されてしまうなどの被害も起きうる可能性があります。

ユーザ自身が怪しいリンクを踏まないように注意することはもちろん必要ですが、開発する側も別なURLから来たリクエストは弾く仕組み作りをするなど対策は必要になってきます。

通常のCSRF対策

よく行われるCSRF対策としては、トークン認証でしょう。

ページを開いたタイミングでサーバよりランダムのトークンを発行してhidden要素に値を埋め込みます。
POSTのタイミングで他の入力値とともにトークンをサーバに送信し、自サイトのサーバから送信されたトークンと照合する方法です。

外部から攻撃するために適当なトークンを送ってPOSTしようとしても、別ドメインからアクセスされたものと判断されて外部からのPOSTを防ぐことができます。

フォームのCSRF対策としては必ずと言っていいほど行われる対策のため、Webの著名なフレームワーク(例:Ruby on Rails、CakePHP)では所定のformタグを埋め込むだけで自動でCSRF対策をしてくれるものが多いです。

さて、それではSPAサイトではどうでしょうか。



SPAサイトのCSRF対策

SPAサイトではページを開いてから非同期で情報を取得してフォームも非同期で作られるため、トークンを仕込むことが難しくなります。
ではSPAサイトではCSRF対策はできず、攻撃されても諦めるしかないのでしょうか?

いえ、そんなことはありません。
HTTPのリクエスト時には、POSTする値以外にも様々な情報を含めて送信できるため、他の方法で自サイトからのアクセスであると照合できれば良いのです。

方法1 リファラで判断

これが一番簡単な方法です。
通常HTTPリクエストでは、リファラ(Referer)を送信することになります。
リファラとは、簡単に言えばアクセス元のページが書かれた情報です。

このリファラが自身のドメインからであれば、正規のリクエストと判断することができます。
ところが、自身のドメインでなければ、外部からの攻撃(CSRF)とみなしてそのリクエストをブロックすればよいのです。
リファラ自体はCSRFの攻撃者では改変できないため、CSRF対策の有効な手段の一つです。

一見容易で問題なさそうに見えるこの方法ですが、弱点も多々あります。
リファラというものは、CSRFの攻撃者からは偽造できませんが、ユーザ自身が別な値にすることは容易です。
例えば以下のようなJavascriptをコンソールで実行するだけで簡単に変更できてしまいます。

Object.defineProperty(document,"referrer",{value:"http://test.com"})


また、ブラウザの設定でリファラを非送信にすることもできるため、不正な利用者でない場合もリファラを送信していないだけではじかれてしまいます。

そしてすべての方法に共通して言えることですが、リファラのチェック部分の実装ミスにも気を使う必要があります。

まずはプロトコルの違いによる部分ですが、httpsからhttpに送信した場合、リファラは送信されません。
API含めた全ページをSSL対応している場合は問題ないですが、対応していない場合はリファラのチェックができない点に注意しましょう。

また、例えば正規表現でチェックをしているとして、「test.comから始まるリファラを通す」という定義をしている場合、test.com.test.comというドメインからリクエストが来た場合も通ってしまいます。
しっかりと「test.com/から始まるリファラを通す」という定義をしましょう。

このように一番簡単に見える方法でも弱点は多いため、この方法を採用するのであれば、リファラのチェックだけで済ませるのではなく、他の対策と併せて使用することをおすすめします。


方法2 ログイン時に発行したトークンを照合

この方法は、ブラウザでPOSTした場合にハッシュ値をチェックするCSRF対策に似ています。

手法としては、ユーザがログインしたタイミングでトークンを発行し、画面表示のタイミングでそのトークンを画面に渡してください。
SPAでステートレスな設計をしており、セッション管理をしていない場合は、トークンを発行したタイミングでユーザとトークンの関係をDBに保存しておくと良いです。

その時、トークンをcookieや、Vue.jsでいうところのstoreなど状態管理のライブラリに保存しておき、POSTのタイミングでサーバに送信します。
サーバ側でトークンを照合し、正しいトークンでなければ処理をブロックすれば良いのです。

この方法で使用するトークンではユーザを特定することもできるため、CSRF対策と同時に、ユーザがログインしているか否かの判別や、ログインユーザの情報取得にも使えます。

この方法も比較的容易ですが、ログインしていることを前提とした方法になっています。
ログインしていなくても、POST処理を使用するようなWebサイトはたくさんあるのではないでしょうか。
匿名の掲示板サイトや新規登録(サインアップ)画面、またはお問い合わせ画面なんかもそうですよね。

ログインしていない場合のPOST処理は重要なものではないとして、チェックしないというサイトの方針の下ではありかと思いますが、できれば方法1のリファラチェックか次に述べる方法3のOriginチェックと組み合わせて使いたいです。

ちなみに…
このトークン認証ではワンタイムトークンを使わなくていいの?と思われる方もいるでしょう。
それに関しては、以下の記事が参考になりました。

egapool.hatenablog.com


方法3 CORSと組み合わせてOriginをチェック

方法1ではヘッダー情報のリファラをチェックしましたが、欠点があることは先に述べた通りです。
今回の方法は、それを補うわけではありませんが、同じヘッダー情報のOriginを使用して照合します。

OriginヘッダーのチェックではCORSと組み合わせる必要があるのですが、そもそもCORSとはなんでしょうか。

またまたそもそもの話になるのですが、SOPをご存知でしょうか?
詳細は以下のページがとても参考になります。

簡単にSOP周辺を理解するページ

CORSとはCross-Origin Resource Sharingの略であり、簡単に言えば他のサイトにAPIなどでアクセスする場合は同じドメイン同士でなければいけないというセキュリティーポリシーです。
ブラウザによって制御されているセキュリティーであり、他ドメインからのアクセスをブロックすることができます。

そもそもブラウザにそのようなセキュリティー対策が備わっているのであれば、もう対策をしなくて大丈夫じゃない?
と思ってしまいがちですが、APIサーバとHTMLを描画するWebサーバを別ドメインにしている場合、話が変わってきます。
SOPの設定により、別のドメインからのアクセスを防いでいるわけですから、正規のWebサーバからのアクセスでもブラウザが弾いてしまいます。


そこでCORSが必要になるわけです。
CORSでは、アクセスを許可するドメインをWhitelist(ホワイトリスト)に指定しておき、同一ドメイン以外からのアクセスであってもWhitelistに指定している場合であればアクセスを許可できるようにブラウザに教えてあげる仕組みです。

ここでWhitelistと照合する情報が、リクエストのヘッダー情報に含まれるOriginになります。
これでWebサーバからのアクセスは弾かれずに済みますね。

また、他ドメインからのアクセスを防げるということは、同時にCSRF対策にもなっているのです。
でもそのOrigin情報が書き換えられたらどうなるの?
と思われる方もいるでしょう。
でもご安心ください。

ドメインをまたいだ(Cross-Origin)リクエストの場合、リクエストのヘッダ情報は書き換えることはできないのです。

このOriginチェックでCSRF対策とはなりますが、自分でカスタムヘッダーを付与してそれで照合させたい場合も出てくるかもしれません。
そのような場合、まずはpreflightリクエストを送り、問題ないリクエストと受け入れられてからリクエストを送ることになります。
以下のあたりの記事がとても参考になりました。

サーバサイドのCORS対応 - Carpe Diem
https://dev.classmethod.jp/etc/about-cors/


イラスト図解式 この一冊で全部わかるセキュリティの基本

イラスト図解式 この一冊で全部わかるセキュリティの基本

基礎から学ぶ Vue.js

基礎から学ぶ Vue.js

Vue.jsとFirebaseで作るミニWebサービス (技術書典シリーズ(NextPublishing))

Vue.jsとFirebaseで作るミニWebサービス (技術書典シリーズ(NextPublishing))