WebEngine

だらだらと綴る技術系メモ

PHPでクロスサイトリクエストフォージェリ対策を実装

クロスサイトリクエストフォージェリCSRF)とは?

ユーザの意図しない操作を、そのユーザの権限で実行させてしまうという恐ろしい攻撃です。CSRFと略称で呼ばれること が多いです。
たとえば、情報発信系サービスであれば、AさんがログインすればAさんしかメッセージを発信できないはずです。しかし、 CSRFの被害にあうと、攻撃者のメッセージがAさんが発信したものとして処理されてしまいます。
ほかにどんな被害を受けるかというと、

  • 意図しない商品購入
  • 意図しない退会処理
  • 意図しない投稿

などが代表的です。

PHPでの対策

ここでは、一般的な対策法をPHPで実装してみます。

<?php

@session_start();

/* POST以外でアクセスされたとき(普通にページに入ってきたときも含まれる) */
if ($_SERVER['REQUEST_METHOD'] != 'POST') {
    setToken();
    checkDump();
/* POSTでアクセスされたとき */
} else {
    checkToken();
    checkDump();
}

/* トークンをセッションにセット */
function setToken() {
    $token = rtrim(base64_encode(openssl_random_pseudo_bytes(32)),'=');
    $_SESSION['token'] = $token;
}

/* トークンをセッションから取得 */
function checkToken() {
    if(empty($_SESSION['token']) || ($_SESSION['token'] != $_POST['token'])) {
        echo 'CSRFの可能性アリ';
    }
}

/* チェック用関数 */
function checkDump() {
    if (isset($_POST['token'])) {
        var_dump($_SESSION['token']);
        var_dump($_POST['token']);
    }
}

?>
<!DOCTYPE html>
<html lang="ja">
<head>
  <title>PHPでCSRF対策</title>
  <meta charset="utf-8">
</head>
<body>
  <form method="post" action="">
    <input type="hidden" name="token" value="<?=htmlspecialchars($_SESSION['token'], ENT_QUOTES, 'UTF-8')?>">
    <input type="submit" value="送信">
  </form>
</body>
</html>

setToken関数でランダムな文字列を生成し、それをhiddenで送信します。その文字列をサーバサイド で管理し、比較するといった流れです。文字列はセッションで管理します。
トークンの生成を

$token = sha1(uniqid(mt_rand(), true));

としているサイトが多いですが、これは理解を容易にするためです。トークン生成には、 暗号論的擬似乱数生成器によるもののほうが安全であるという意見があり、今回はあえて そちらで実装をしてみました。mcrypt_create_ivを利用しても良いですね。
なお、openssl_random_pseudo_bytesはPHP5.3.0以降でないと使えない のであしからず。

登録ボタンを押したところ、僕の場合、実行結果(ダンプ処理の結果)は

string 'j7C6ES2rXWczWtM/2mHP3EL/YIJcGbQsuv6jPMEq7xE' (length=43)
string 'j7C6ES2rXWczWtM/2mHP3EL/YIJcGbQsuv6jPMEq7xE' (length=43)

と表示されました。ちゃんと正規のリクエストなので文字列が一致しているのが確認できます。

不正リクエストを送ってみる

念のため、GETでアクセスされたときどうなるのかも見てみましょう。フォームの送信手法を

<form method="get" action="">

と変更します。それからチェック用につくった関数であるcheckDump関数のPOSTを

function checkDump() {
    if (isset($_GET['token'])) {
        var_dump($_SESSION['token']);
        var_dump($_GET['token']);
    }
}

のようにGETに変更します。この関数は単なるチェック用なのでCSRF対策用の関数に 影響を与えることはありません。

僕の場合は

string 'PcCRYNLU9U2hNV8hlc36C27KitYAGJ9+Kkp28d7+cZo' (length=43)
string 'EwoPwaGwbhdQyyUQxLI1jcDLf36EN6IaDg023xmd+uk' (length=43)

と表示されました。ちゃんと異なる文字列になっていることがわかります。

参考