ポートフォリオを公開してたらサイバー攻撃された話 〜Laravelで多重送信対策をしよう〜

## 初めに

去年の今頃、私は未経験からサーバーサイドエンジニアを目指して転職活動をしていて、
Laravel6系で開発したポートフォリオをインターネット上に公開していました。

実装のツメが甘い部分もあり、いろんなイタズラをされては対策して..を繰り返していましたが、
ある日容赦ない<font color="Red">サイバー攻撃</font>をくらってしまい、サーバーが死にかけるという経験をしたので
今回はそんな当時の出来事と、どう対策したかを記事にしてみようと思います😇

現在 転職活動でポートフォリオを作成されている方や、プログラミング学習中の方のご参考になれば幸いです🙏

## 最初はまだイタズラレベルだった..

当時開発していたポートフォリオは、朝活をテーマにしたSNSアプリでした。
twitterみたいに、ユーザーが投稿できる機能があります。

![スクリーンショット 2021-01-15 22.59.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/613419/87067860-ed1d-0e9e-700b-0eb2a189f8f9.png)

上記の画像を見てみると、**「こんにちは」**という投稿が`同一時刻`で複数投稿されていますね。
これ、同じ人が連続で同じ投稿をしているんです。
他にも、「あああああ」といったものや下ネタ投稿が連続投稿されたりしていました。
最初は５, ６件とかだったのですが、だんだんと**１０件, ２０件**と連続投稿の数が増えてくるようになり..

おかしいなと思ったら、
投稿画面の**「送信ボタン」**等に問題がありまして、このボタンをクリック連打すると
重複してリクエストが送られ、<font color="IndianRed">連続投稿できてしまう状態だったんです</font>。
(CSSにBootstrapとかを使っていたりするとなりやすい？)

↓再現GIF動画
![tajutokodayo.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/613419/757fe541-135b-d6e1-c35f-9e74e22f10d0.gif)

うーん、これはいかんな・・と思い、早速対策をすることにしました。


## 多重投稿できないよう対策してみたが..

### サーバーサイド側で対策（超簡単）

まずはLaravel側で対策。
投稿を保存する処理は、当初はざっくり以下のような処理を書いていました。（簡略化してます）

**Before**

```php:ArticleController.php
    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**

```php:ArticleController.php
    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対策の手法としては、
正規のリクエストかを判断するために投稿の入力画面が開かれる時に、サーバー側は機密情報(ワンタイムパスワード)の<font color="ForestGreen">**トークン**</font>を生成して、このトークンを以下の２つの場所に保存します。

- サーバーのセッションに保存
- 入力画面のHTMLの**< input type="hidden" >**タグののvalueに保存　←bladeに記述する@csrfの実体

そして、入力画面から実際に「送信ボタン」がクリックされたら、
入力されたデータと一緒に**トークン**がサーバーに送信されます。
ここで、<font color="Salmon">送信されたトークンとサーバーのセッションに保存されたトークンが一致していれば</font>
正規のリクエストとみなし、そうでなければ**419エラー**が返されます。

今回コードに追加した

```PHP
$request->session()->regenerateToken(); 
```
の処理では、「送信ボタン」がクリック連打されて1回目のリクエストの処理が完了した後に
サーバー側の<font color="ForestGreen">トークンを作り直しているので</font>、
重複した2回目のリクエストが来た時には送信されたトークンとサーバーに保存されているトークンが一致せず、
419エラーを返すようになるわけです。

↓「送信ボタン」をクリック連打した結果
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/613419/c6ad842e-bac3-e8b4-a0dd-4ba7aba590b3.png)



### フロントエンド側でも対策

次に、念の為フロント側の対策もjQueryでしてみました。
「送信ボタン」が1度クリックされたら、< button >タグを非活性にして2回目以降クリックできなくするというシンプルなものです。

```javascript
$(function(){
  // 送信ボタンが1度クリックされたら、送信ボタンを非活性化する（二重submit対策）
  $('form').submit(function() {
    $("button[type='submit']").prop("disabled", true);
  });
});
```

## ところが、サイバー攻撃を受ける😭

サーバーサイドとフロントの両方で対策もできたし、これでもう大丈夫だろう！😄
と思ったのも束の間、
まだ対策が足りませんでした。

ある日、なんと数千件レベルの<font color="Red">大量リクエスト</font>が送られてきたのです。。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/613419/4ab5d708-7227-c1ac-76b7-af155701439c.png)

投稿一覧を開いてみると、**「おっっっっっぴ」**という投稿がたくさんあることに気付きました。
投稿を下にスクロールしていっても、無限に**「おっっっっっぴ」**が現れます。
一体何件あるんだ？！と思っていたら、サイトがかなり重くなっている..
終いには、全く反応しなくなってしまいました😱

当時サーバーはAWSのEC2を利用していて、DBはRDSを利用していたのですが、
DBに接続してデータを確認してみることにしました。

![スクリーンショット 2021-01-15 21.31.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/613419/26d6bbd7-59bd-a2bb-bf24-7112750f282b.png)
![スクリーンショット 2021-01-15 22.58.19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/613419/eacd588d-f7cd-07e5-5e53-b1f4321e7c2a.png)

すると、当時にスクショを撮った時点では<font color="Red">781件</font>の**「おっっっっっぴ」**がDBに登録されており、ここから更にものすごい勢いで
**1,000件、2,000件**と増えていきました。

短時間で大量のリクエストが送られてきていて、もはや<font color="Red">Dos攻撃</font>。。

これはマズイ。。と思い、急いでEC2を停止して更なる対策をすることにしました。

## レート制限をかけて対策！（超簡単）

多重送信対策はしたはずなのに、どうしてここまで大量のリクエストを送信できたんだろう。。？🤔

おそらくですが、悪意のあるユーザーがツール等を使って

- 投稿の入力画面にアクセス→送信ボタンをクリック

の処理を大量に行なっていたのではないかと思います。（スクレイピングの要領？）

なので、ここは<font color="Red">**「レート制限」**</font>をかけて
サーバーサイド側で**１分間に受け付けられるリクエストの数に制限をかける**ことにしました。
（レートリミットと言ったりもします）

このレート制限をかけるためのミドルウェアはLaravelで用意されているので、使い方はとても簡単です！

参考：

- [レート制限とは？(公式ドキュメント)](https://laravel.com/docs/6.x/routing#rate-limiting)
- [Laravelのレートリミットミドルウェアについて調べた](https://blog.pinkumohikan.com/entry/laravel-rate-limit-middleware)

例えば、「**１分間**に**最大3回**までリクエストを受け付ける」といった制限をかける場合は、

レート制限をかけたい処理のルーティングに<font color="ForestGreen">throttleミドルウェア</font>を指定します。

**Before**

```php:web.php
// ユーザー投稿の保存処理
Route::resource('/articles', 'ArticleController')
    ->only(['store'])

```

**After**

```php:web.php
// ユーザー投稿の保存処理
Route::resource('/articles', 'ArticleController')
    ->only(['store'])
    ->middleware('throttle:3, 1'); // <- この一行を追加：　1分間に最大3回までリクエストを受け付ける

```

今回、
`->middleware('throttle:3, 1');` の一行を追加しましたが、
'**throttle:**`$何回まで?`, `$何分間のうちに?`'　といった形式でリクエスト回数の上限と分を指定できます。

### レート制限がかかったか試してみた

今回、「**１分間**に受け付けるリクエストは**最大3回**まで」というレート制限をかけたので、
１分間に<font color="Red">4回</font>以上リクエストが送れなくなったかを検証してみました。

- 投稿画面を開く　→　メッセージを送信

といった一連の処理を１分以内に4回行おうとしたところ、
<font color="Red">4回目</font>の「送信ボタン」クリックがされた時に**429エラーが返ることが確認できました**！✨

↓レート制限の検証GIF動画
![kensho.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/613419/7fa38cec-551d-f394-e964-faa62bfb316d.gif)

こうしてなんとか、Dos攻撃対策を実装することができ、
私のポートフォリオには平和が訪れたのでした。。

（レート制限をもっと厳格にかける場合は、IPアドレスでの制限もできるようです！）


## 終わりに

今回は、１年くらい前に経験したサイバー攻撃とその対策についてを記事にしてみました。
この記事ではPHP/Laravelでのケースについて書いていますが、多重送信対策に関しては他の言語でも必要になってくるものだと思います。

Laravelの場合、**CSRFトークンの再生成**や、**レート制限のミドルウェア設定**など、いずれもたった一行のコード追加で
対策できるくらい簡単なものなので、ぜひご参考になれば幸いです。
最後までお読みいただき、ありがとうございました！