クロスサイトリクエストフォージェリ

## はじめに

クロスサイトリクエストフォージェリについてまとめる。

### クロスサイトリクエストフォージェリとは

サイトに攻撃用のスクリプトを仕込むことでアクセスしてきたユーザーに意図しない操作を強制すること。

具体的な被害例として、「あるサービスに勝手に登録させられてしまう」、「掲示板やブログに勝手に書き込みされてしまう」「ショッピングサイトで自動的に物品を購入されてしまう」のような被害を受ける可能性がある。

### 例

以下のようなコードで書かれたサイトを運営していたとして、

```php:insert_form.php
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <title>データの登録</title>
</head>

<body>
    <form action="insert.php" method="POST">
        <div>
            <label for="isbn">ISBNコード：</label>
            <input id="isbn" name="isbn" type="text" siza="25" maxlength="20">
        </div>
        <div>
            <label for="title">書名：</label>
            <input id="title" name="title" type="text" siza="35" maxlength="150">
        </div>
        <div>
            <label for="price">価格：</label>
            <input id="price" name="price" type="text" siza="5" maxlength="5">円
        </div>
        <div>
            <label for="publish">出版社：</label>
            <input id="publish" name="publish" type="text" siza="15" maxlength="30">
        </div>
        <div>
            <label for="published">刊行日：</label>
            <input id="published" name="published" type="text" siza="15" maxlength="10">
        </div>
        <div>
            <input type="submit" value="登録">
        </div>
    </form>
</body>

</html>
```

```php:insert_process.php
<?php
require_once __DIR__ . '../DbManager.php';

try {
    $db = getDb();
    $stt = $db->prepare('INSERT INTO book(isbn, title, price, publish, published) 
    VALUES(:isbn, :title, :price, :publish, :published)');
    $stt->bindValue(':isbn', '$_POST[isbn]');
    $stt->bindValue(':title', '$_POST[title]');
    $stt->bindValue(':price', '$_POST[price]');
    $stt->bindValue(':publish', '$_POST[publish]');
    $stt->bindValue(':published', '$_POST[published]');
    $stt->execute();
    header('Location: http://'.$_SERVER['HTTP_HOST'].dirname($_SERVER['PHP_SELF']).'/insert_form.php');
} catch (PDOException $e) {
    die("エラーメッセージ:{$e->getMessage()}");
}
```

```php:DbManager.php
<?php
function getDb(): PDO
{
    $dsn = 'mysql:dbname=selfphp; host=127.0.0.1; charset=utf8';
    $usr = 'selusr';
    $passwd = 'selfpass';

    $db = new PDO($dsn, $usr, $passwd);
    return $db;
}
```

第三者が以下のような攻撃者のページにアクセスすると、

```untrue.html
ようこそ、いらっしゃいました！
<iframe src="auto_submit.html" width="1" height="1"></iframe>
```

```html:auto_submit.html
<html>
<body onload="document.fm.submit();">
    <form name="fm" method="POST" action="http://localhost/insert_process.php">
        <input type="hidden" name="isbn" value="978-X-7X80-2401-X" />
        <input type="hidden" name="title" value="Bad Book" />
        <input type="hidden" name="price" value="9999" />
        <input type="hidden" name="publish" value="Bad Publish" />
        <input type="hidden" name="published" value="2016-12-31" />
    </form>
</body>
</html>
```

`insert_process.php`に意図しないデータを送信してしまう。
(インラインフレームの大きさ的にユーザーは何が起こっているのか気づかない)

### 対策

ワンタイムトークン方式を使う。

ランダムな文字列(トークン)を入力フォーム(`insert_form.php`)に隠しフィールドとして埋め込み、セッションにも保存する。

```php:insert_form.php
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <title>データの登録</title>
</head>

<body>
    <form action="insert.php" method="POST">
        <div>
            <label for="isbn">ISBNコード：</label>
            <input id="isbn" name="isbn" type="text" siza="25" maxlength="20">
        </div>
        <div>
            <label for="title">書名：</label>
            <input id="title" name="title" type="text" siza="35" maxlength="150">
        </div>
        <div>
            <label for="price">価格：</label>
            <input id="price" name="price" type="text" siza="5" maxlength="5">円
        </div>
        <div>
            <label for="publish">出版社：</label>
            <input id="publish" name="publish" type="text" siza="15" maxlength="30">
        </div>
        <div>
            <label for="published">刊行日：</label>
            <input id="published" name="published" type="text" siza="15" maxlength="10">
        </div>
        <div>
            <input type="submit" value="登録">
        </div>
        <!-- 追記 ここから-->
        <?php
        session_start();
        $token = uniqid(bin2hex(random_bytes(13)), true);
        $_SESSION['token'] = $token;
        ?>
        <input type="hidden" name="token" value="<?php print $token; ?>">
        <!-- 追記 ここまで-->
    </form>
</body>

</html>
```

処理側(`insert_process.php`)ではフォームからトークンが送信されているか、送信されていたらセッションに保存された値とポストデータが等しいかチェックする。

```php:insert_process.php
<?php
require_once __DIR__ . '../DbManager.php';

// 追記 ここから
session_start();
if (!isset($_POST['token']) || $_POST['token'] !== $_SESSION['token']) {
    die('不正なアクセスが行われました');
}
// 追記 ここまで

try {
    $db = getDb();
    $stt = $db->prepare('INSERT INTO book(isbn, title, price, publish, published) 
    VALUES(:isbn, :title, :price, :publish, :published)');
    $stt->bindValue(':isbn', '$_POST[isbn]');
    $stt->bindValue(':title', '$_POST[title]');
    $stt->bindValue(':price', '$_POST[price]');
    $stt->bindValue(':publish', '$_POST[publish]');
    $stt->bindValue(':published', '$_POST[published]');
    $stt->execute();
    header('Location: http://' . $_SERVER['HTTP_HOST'] . dirname($_SERVER['PHP_SELF']) . '/insert_form.php');
} catch (PDOException $e) {
    die("エラーメッセージ:{$e->getMessage()}");
}

```

### 参照

書籍名：独習PHP 第4版
著者名：山田祥寛
出版社名：翔泳社