初めに
去年の今頃、私は未経験からサーバーサイドエンジニアを目指して転職活動をしていて、
Laravel6系で開発したポートフォリオをインターネット上に公開していました。
実装のツメが甘い部分もあり、いろんなイタズラをされては対策して..を繰り返していましたが、
ある日容赦ないサイバー攻撃をくらってしまい、サーバーが死にかけるという経験をしたので
今回はそんな当時の出来事と、どう対策したかを記事にしてみようと思います😇
現在 転職活動でポートフォリオを作成されている方や、プログラミング学習中の方のご参考になれば幸いです🙏
最初はまだイタズラレベルだった..
当時開発していたポートフォリオは、朝活をテーマにしたSNSアプリでした。
twitterみたいに、ユーザーが投稿できる機能があります。
上記の画像を見てみると、「こんにちは」という投稿が同一時刻
で複数投稿されていますね。
これ、同じ人が連続で同じ投稿をしているんです。
他にも、「あああああ」といったものや下ネタ投稿が連続投稿されたりしていました。
最初は5, 6件とかだったのですが、だんだんと10件, 20件と連続投稿の数が増えてくるようになり..
おかしいなと思ったら、
投稿画面の**「送信ボタン」**等に問題がありまして、このボタンをクリック連打すると
重複してリクエストが送られ、連続投稿できてしまう状態だったんです。
(CSSにBootstrapとかを使っていたりするとなりやすい?)
うーん、これはいかんな・・と思い、早速対策をすることにしました。
多重投稿できないよう対策してみたが..
サーバーサイド側で対策(超簡単)
まずはLaravel側で対策。
投稿を保存する処理は、当初はざっくり以下のような処理を書いていました。(簡略化してます)
Before
public function store(ArticleRequest $request): RedirectResponse
{
$user = auth()->user();
$data = $request->all();
// 投稿をDBに保存
$user->articles()->create($data);
return redirect()->route('articles.index');
}
このstore()メソッドに、たった一行
$request->session()->regenerateToken();
を追加して、次のように更新しました。
After
public function store(ArticleRequest $request): RedirectResponse
{
$user = auth()->user();
$data = $request->all();
// 投稿をDBに保存
$user->articles()->create($data);
// CSRFトークンを再生成して、二重送信対策
$request->session()->regenerateToken(); // <- この一行を追加
return redirect()->route('articles.index');
}
ここで何をしているのかというと..
CSRF対策の仕組みを利用しています。
通常、CSRF対策の手法としては、
正規のリクエストかを判断するために投稿の入力画面が開かれる時に、サーバー側は機密情報(ワンタイムパスワード)のトークンを生成して、このトークンを以下の2つの場所に保存します。
- サーバーのセッションに保存
- 入力画面のHTMLの**< input type="hidden" >**タグののvalueに保存 ←bladeに記述する@csrfの実体
そして、入力画面から実際に「送信ボタン」がクリックされたら、
入力されたデータと一緒にトークンがサーバーに送信されます。
ここで、送信されたトークンとサーバーのセッションに保存されたトークンが一致していれば
正規のリクエストとみなし、そうでなければ419エラーが返されます。
今回コードに追加した
$request->session()->regenerateToken();
の処理では、「送信ボタン」がクリック連打されて1回目のリクエストの処理が完了した後に
サーバー側のトークンを作り直しているので、
重複した2回目のリクエストが来た時には送信されたトークンとサーバーに保存されているトークンが一致せず、
419エラーを返すようになるわけです。
フロントエンド側でも対策
次に、念の為フロント側の対策もjQueryでしてみました。
「送信ボタン」が1度クリックされたら、< button >タグを非活性にして2回目以降クリックできなくするというシンプルなものです。
$(function(){
// 送信ボタンが1度クリックされたら、送信ボタンを非活性化する(二重submit対策)
$('form').submit(function() {
$("button[type='submit']").prop("disabled", true);
});
});
ところが、サイバー攻撃を受ける😭
サーバーサイドとフロントの両方で対策もできたし、これでもう大丈夫だろう!😄
と思ったのも束の間、
まだ対策が足りませんでした。
ある日、なんと数千件レベルの大量リクエストが送られてきたのです。。
投稿一覧を開いてみると、**「おっっっっっぴ」という投稿がたくさんあることに気付きました。
投稿を下にスクロールしていっても、無限に「おっっっっっぴ」**が現れます。
一体何件あるんだ?!と思っていたら、サイトがかなり重くなっている..
終いには、全く反応しなくなってしまいました😱
当時サーバーはAWSのEC2を利用していて、DBはRDSを利用していたのですが、
DBに接続してデータを確認してみることにしました。
すると、当時にスクショを撮った時点では781件の**「おっっっっっぴ」**がDBに登録されており、ここから更にものすごい勢いで
1,000件、2,000件と増えていきました。
短時間で大量のリクエストが送られてきていて、もはやDos攻撃。。
これはマズイ。。と思い、急いでEC2を停止して更なる対策をすることにしました。
レート制限をかけて対策!(超簡単)
多重送信対策はしたはずなのに、どうしてここまで大量のリクエストを送信できたんだろう。。?🤔
おそらくですが、悪意のあるユーザーがツール等を使って
- 投稿の入力画面にアクセス→送信ボタンをクリック
の処理を大量に行なっていたのではないかと思います。(スクレイピングの要領?)
なので、ここは「レート制限」をかけて
サーバーサイド側で1分間に受け付けられるリクエストの数に制限をかけることにしました。
(レートリミットと言ったりもします)
このレート制限をかけるためのミドルウェアはLaravelで用意されているので、使い方はとても簡単です!
参考:
例えば、「1分間に最大3回までリクエストを受け付ける」といった制限をかける場合は、
レート制限をかけたい処理のルーティングにthrottleミドルウェアを指定します。
Before
// ユーザー投稿の保存処理
Route::resource('/articles', 'ArticleController')
->only(['store'])
After
// ユーザー投稿の保存処理
Route::resource('/articles', 'ArticleController')
->only(['store'])
->middleware('throttle:3, 1'); // <- この一行を追加: 1分間に最大3回までリクエストを受け付ける
今回、
->middleware('throttle:3, 1');
の一行を追加しましたが、
'throttle:$何回まで?
, $何分間のうちに?
' といった形式でリクエスト回数の上限と分を指定できます。
レート制限がかかったか試してみた
今回、「1分間に受け付けるリクエストは最大3回まで」というレート制限をかけたので、
1分間に4回以上リクエストが送れなくなったかを検証してみました。
- 投稿画面を開く → メッセージを送信
といった一連の処理を1分以内に4回行おうとしたところ、
4回目の「送信ボタン」クリックがされた時に429エラーが返ることが確認できました!✨
こうしてなんとか、Dos攻撃対策を実装することができ、
私のポートフォリオには平和が訪れたのでした。。
(レート制限をもっと厳格にかける場合は、IPアドレスでの制限もできるようです!)
終わりに
今回は、1年くらい前に経験したサイバー攻撃とその対策についてを記事にしてみました。
この記事ではPHP/Laravelでのケースについて書いていますが、多重送信対策に関しては他の言語でも必要になってくるものだと思います。
Laravelの場合、CSRFトークンの再生成や、レート制限のミドルウェア設定など、いずれもたった一行のコード追加で
対策できるくらい簡単なものなので、ぜひご参考になれば幸いです。
最後までお読みいただき、ありがとうございました!