LaravelでCSRFを利用した多重送信対策とブラウザリロード対策
5206 回閲覧されました
みなさんこんにちは、jonioです。
LaravelでECサイトを作っていた時に購入ボタンを押してから画面リロードをするとエラーになったり・ブラウザバック(前のページに戻る)してから購入ボタンを押すと購入した物をまた購入できる現象が起きました。
画面リロードに関してはルーティングを追記する事でエラーの回避ができました(後で説明します)がブラウザバックに関してはCSRF(クロスサイトリクエストフォージェリ)対策用のトークンを発行する事で無効にできる事が分かりました。
今回はCSRFとは何かと多重送信対策・画面リロード対策の方法を解説をします。
調べた内容なので間違っていたら申し訳ありません。
csrfとは
csrfはクロスサイトリクエストフォージェリの略でWEBアプリの脆弱性を突いた攻撃方法です。
どんな被害があるかを例で見ていきます。
ある投稿アプリがあったとしてそのアプリはセキュリティ対策をされていなかったとします。
そのサイトに攻撃者が怪しいリンクを貼った投稿をしました。
このサイトにあるユーザーがログインして怪しいリンクをクリックしました。
するとそのユーザーのログイン情報が盗まれて攻撃者から勝手に変な内容の投稿がされました。
例の被害は勝手に投稿をされていますがやろうと思えば他の事もできます。
これがcsrfです。
Laravelにはcsrf対策が入っていますがトークンと言うランダムな文字列を使います。
csrf対策
例えばフォームがあるページの「http://localhost:8888/index」にアクセスしたとします。
この時にトークンと言うランダムな40文字の文字列が生成されてトークンはsessionとformタグの中のinputタグに付いているname属性に保存されます。
またトークンですがページにアクセスする度に別のトークンが作成されます。
この時にformタグのコードは↓になります。
3行目のinputタグは自動的に生成されます。
トークンは3行目の「Fzshww7Z8M9doUpT5Az09f96lr5i7jDtMPArf9ob」です。
3行目のinputタグのtype属性はhiddenになりますがこれは2行目の「@csrf」で行っています。
そしてフォームを送信した時にinputタグの中のトークン(Fzshww7Z8M9doUpT5Az09f96lr5i7jDtMPArf9ob)とsessionに保存されているトークンが同じかを確認して同じならフォームの送信ができます。
csrfができない理由
トークンはランダムな文字列で生成されてページを見るたびに文字列が変わるのですがログインしている人のトークンをxxxとします。
ログインした人の情報を盗んだ攻撃者がログインして投稿しようとしても別のトークンが作成されてトークンが違う(xxxではない)から攻撃する事ができません。
これがcsrfができない理由です。
それではここからは多重送信対策の解説ですがトークンが変わるのを利用します。
ブラウザバック対策の考え方
↓の「購入する」ボタンで購入したとします。
そして購入ページに飛んでブラウザバック(前のページに戻る)とまた↑のページが表示されてブラウザバックの対策をしていない場合再購入ができます。
ECサイトの場合だとこれは機能としてまずいのでブラウザバックをした場合に再購入ができず↓の表示がされる様にします。
ブラウザに表示されている「419 | PAGE EXPIRED」とは「csrfという不正なフォーム送信ですよ」という意味です。
購入するボタンを機能させるコントローラーは↓です。
これに1行を追加します。
8行目は「購入するボタンを押したらトークンを新しく作ります」という意味です。
これだけで完成ですがなぜこれでいいのかを説明します。
購入するボタンを押す前のトークンをxxxとします。
購入するボタンを押した時点でトークンが別の文字列(yyyとします)に変わりますがブラウザバックするとトークンはxxxのままです。
でもトークンはyyyに変わっているのでcsrfとみなされて419 | PAGE EXPIREDになるという流れです。
購入するボタンを連打するのを防ぐ
↓の購入するボタンを押した時に画面の読み込みがしばらく続くと購入するボタンが動いているのに動いていないと思い込んでもう一度ボタンを押す可能性があります。
ECサイトの場合だと二重購入になってサイトの仕様として致命的なのでボタン連打は防がないといけません。
購入するボタンを一度押したら押せないようにすればいいのでJavaScriptで制御すればいいと思いました。
購入するボタンがあるページで購入するボタンをクリックした時にJavaScriptでcssを付加してボタンをクリックできなくする為にJavaScriptのコードを↓にします。
また付加するcssを↓にします。
これで購入するボタンをクリックすると↓の見た目になります。
ボタンが薄くなるのとクリックできなくなるのでいい感じだと思います。
cssに「pointer-events:none;」があるのでボタンを2回目以降のクリックができなくなります。
2回目以降のクリックができなくなる状態は保存されないのでまたサイトを訪れて購入する際は購入するボタンを押せる様になっています。
購入ページをリロードした時のエラー対策
購入するボタンを押すと購入したページに移動します。
この時のルーティングは↓です。
コントローラーは↓です。
この時に購入が完了したページのリロードをすると↓のエラーが出ます。
このページを表示する時のhttpメソッドはGETになりますがルーティングを見るとPOSTなのでエラーになります。
ネットを調べても分からなかったので自分で考えたのですが画面をリロードしたら別のページに飛ばせばいいと思いました。
ルーティングを↓にします。
コントローラーを↓にします。
これで完成です。