【Php】Csrf対策ー学習ノート

# 初めに
phpのCsrf対策について学習した内容のoutput用記事です。

※内容に間違いなどがある場合はご指摘をよろしくお願いします。
※こちらの記事はあくまでも個人で学習した内容のoutputとしての記事になります。

前回の記事：https://qiita.com/redrabbit1104/items/a6e57aa1fd1771ef90ff

# Csrfとは
Csrfはクロスサイトリクエストフォージェリ(Cross-Site Request Forgery)の略で、本物そっくりなWebサイトを用意しユーザーからのログインなどを誘導。不正なリクエストなどを要求されたりして、利用者の個人情報や公開されてはいけないデータなどを盗み取る攻撃手法です。

# 対策方法→ sessionを利用する
\$_POSTや\$_GETなどのHTTP通信に使われるスーパグローバル変数は一度きりのものであり利用ごは削除されてしまいます。それに対して\$_SESSIONはずっとデータがずっと残り続けます。この\$_SESSIONを合言葉として使えば本物かどうかをチェックすることができます。

# 使ってみる
sessionを使うにはまずsession_start()関数を使ってsessionをスタートさせる必要があります。これをphpファイルの冒頭に記述します。

```php
session_start();
```
次に合言葉を作成します。前回作った入力フォームの中から入力の際に確認を取りたいので、入力画面に合言葉を生成することにします。

# random_bytes()関数
phpのマニュアルには
>暗号論的に安全な、疑似ランダムなバイト列を生成する

となっています。24~32までの数字を引数にして作成しますが、今回は32にしてみます。そして中身が気になるので、echoでブラウザーに表示してみます。

```php
<?php
  echo random_bytes(32);
?>
```
訳のわからない機械言語みたいなものが表示されます。
![スクリーンショット 2021-06-14 16.59.27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/966135/076683f8-0dac-1d15-c5f0-d4050880d07e.png)

# bin2hex()関数

これを人が読めるような数字にしたいので、bin2hex()関数を使って16進数に変えます。

```php
<?php
  echo bin2hex(random_bytes(32));
?>
```
英語と数字が混ざっている16真数の数字になりました。(0~9,a~fまでの英数字)
![スクリーンショット 2021-06-14 17.03.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/966135/58f36e4f-09fa-b9a0-7025-c96daac5615f.png)
この数字がtokenになります。これを\$csrfTokenという変数に格納します。

```php
$csrfToken = bin2hex(random_bytes(32));
```

# $_SESSIONに$csrfTokenを保存しておく
\$_SESSIONは連想配列の構造になっています。これはsession_start()関数によって生成されます。この\$_SESSIONにキー名を'csrfToken'にして、先ほど生成した16進数の英数字$csrfTokenを格納します。そして、isset()関数を使い\$_SESSION['csrfToken']がセットされていない場合に\$csrfTokenを格納するようにします。

```php
if(!isset($_SESSION['csrfToken'])){
    $csrfToken = bin2hex(random_bytes(32));
    $_SESSION['csrfToken'] = $csrfToken;
}
```
また、\$_SESSION['csrfToken']は長いので、$tokenという名前の変数に再代入します。

```php
$token = $_SESSION['csrfToken'];
```

# 入力画面で$tokenをhiddenタイプで$tokenを渡しておく
typeを"hidden"にしフォームに表示されない形で\$tokenの値をpostします。nameは"csrf"にし、valueにはphpのechoで\$tokenを渡します。

```php
<input type="hidden" name="csrf" value="<?php echo $token; ?>">
```
完成した入力画面は以下の通り。これで$_SESSIONにも$_POSTにも同じ値が格納されていることになります。

```php
   <?php if ($page_flag === 0) : ?>
        <?php
        if (!isset($_SESSION['csrfToken'])) {
            $csrfToken = bin2hex(random_bytes(32));
            $_SESSION['csrfToken'] = $csrfToken;
        }
        $token = $_SESSION['csrfToken'];
        ?>
        入力画面
        <form method="POST" action="csrf.php">
            名前
            <input type="text" name="input_name">
            <br>

            <input type="submit" name="btn_submit" value="submit">
            <input type="hidden" name="csrf" value="<?php echo $token; ?>">
        </form>
    <?php endif; ?>
```

#　確認画面で$_POSTの値と$_SESSIONの値が同じ場合にのみ、画面表示を行う
入力画面のPOSTの値として\$tokenを渡しています。それと$_SESSIONの中に保存されている値が一致する場合のみ、確認画面の処理が行われるようにします。一致しないということは本物のWebサイトの入力画面で発行した\$_SESSIONがないということであり、不正なサイトから誘導されたことになるわけです。

```php
<?php if ($_POST['csrf'] === $_SESSION['csrfToken']) : ?>
```
また、確認画面でもhiddenタイプで\$_POSTの値を渡すようにします。これは後ほど出てくる完了画面で\$_SESSIONの値と$_POSTの値が一致するかどうかを条件分岐によって判断するからです。\$_POSTは一回きりの値なので入力画面で渡したとしても、消えてしまいます。なので、再度hiddenタイプで渡す必要があります。

```php
<input type="hidden" name="csrf" value="<?php echo sp_chars($_POST['csrf']); ?>">
```
出来上がった確認画面は以下の通りになります。

```php
    <?php if ($page_flag === 1) : ?>
        <?php if ($_POST['csrf'] === $_SESSION['csrfToken']) : ?>


            確認画面
            <form method="POST" action="csrf.php">
                名前
                <?php echo sp_chars($_POST["input_name"]); ?>
                <br>

                <input type="submit" name="btn_confirm" value="confirm">
                <input type="hidden" name="input_name" value="<?php echo sp_chars($_POST['your_name']); ?>">
                <input type="hidden" name="csrf" value="<?php echo sp_chars($_POST['csrf']); ?>">
            </form>
        <?php endif; ?>

    <?php endif; ?>
```

# 完成画面
\$_POSTの値と\$_SESSIONの値が一致する場合にのみ、完了メッセージが表示するようにします。

```php
 <?php if ($_POST['csrf'] === $_SESSION['csrfToken']) : ?>
```
また、unset()メソッドを使い、\$_SESSIONの値を破棄します。理由としてはずっと古いままの\$_SESSIONの値が残り続けていると新しく投稿する際に同じtokenの値になってしまうため、セキュリティ上問題になるからです。

```php
<?php unset($_SESSION['csrfToken']); ?>
```
これで完了画面の出来上がりです。

```php
    <?php if ($page_flag === 2) : ?>
        <?php if ($_POST['csrf'] === $_SESSION['csrfToken']) : ?>
            送信完了。
            <?php unset($_SESSION['csrfToken']); ?>
        <?php endif; ?>
    <?php endif; ?>
```
# 参考サイト
https://siteguard.jp-secure.com/blog/what-is-csrf
https://www.php.net/manual/ja/function.random-bytes.php