Webセキュリティの小部屋

Twitter のフォローはこちらから Facebook ページはこちら RSSフィードのご登録はこちらから
公開日:2013年3月7日
最終更新日:2020年8月18日

クロスサイト・リクエストフォージェリ(CSRF)対策(PHP編)

入力→確認→登録→結果 という画面遷移を行う Webアプリケーションを作成し、PHP におけるCSRF 対策の具体例を紹介します。画面遷移は下図のようになります。

なお、トークンをセットする画面が登録確認画面だけなのは、CSRF 対策は重要な処理を行う画面に組み込むものだからです。但し、入力画面にもトークンをセットするのは間違いではありません。

画面遷移図

以前はトークンにセッション ID を使用することが多かったですが、最近は暗号論的擬似乱数生成器で作成した乱数をトークンに使用します。トークンについて詳しくは以下の記事を参照してください

・function.php

共通関数を格納しているファイルです。CSRF のトークンもここの関数で作成します。

<?php
define("DNS","mysql:host=localhost;dbname=registersample;charset=utf8");
define("USER_NAME", "mysql");
define("PASSWORD", "Mysql@1234");

/*
* CSRF トークン作成
*/
function get_csrf_token() {
  $TOKEN_LENGTH = 16;//16*2=32byte
  $bytes = openssl_random_pseudo_bytes($TOKEN_LENGTH);
  return bin2hex($bytes);
}
/*
* PDO の接続オプション取得
*/
function get_pdo_options() {
  return array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
               PDO::MYSQL_ATTR_MULTI_STATEMENTS => false,
               PDO::ATTR_EMULATE_PREPARES => false);
}

 

・input.php

入力画面です。

<!DOCTYPE html>
<html lang="ja">
<body>
  <h1>名前を入力してください</h1>
  <form action="confirm.php" method="post">
    名前:<input type="text" name="name"><br>
    <input type="submit" value="送信">
    <input type="reset" value="リセット">
  </form>
</body>
</html>

 

・confirm.php

登録確認画面です。

<?php
  session_start();
  require_once('function.php');
  $name = $_POST['name'];
  $_SESSION['token'] = get_csrf_token(); // CSRFのトークンを生成しセッションに格納
?>
<!DOCTYPE html>
<html lang="ja">
<body>
  <h1>登録しますか?</h1>
  <form action="register.php" method="post">
    名前:<?php echo htmlspecialchars($name, ENT_QUOTES, "UTF-8"); ?><br>
    <input type="submit" value="登録">
    <input type="button" value="戻る" onclick="history.back();">
    <input type="hidden" name="name" value="<?php echo htmlspecialchars($name, ENT_QUOTES, "UTF-8"); ?>">
    <input type="hidden" name="token" value="<?php echo htmlspecialchars($_SESSION['token'], ENT_QUOTES, "UTF-8") ?>">
  </form>
</body>
</html>

 

・register.php
登録処理です。

<?php
  session_start();
  require_once('function.php');

  // 値取得
  $name = $_POST['name'];
  $token = $_POST['token'];

  //CSRFチェック
  if ($token != $_SESSION['token']) {
    //CSRFエラー画面へ
    header('HTTP/1.1 301 Moved Permanently');
    header('Location: error.php');
    exit();
  }

  //通常リクエスト

  try {
    $pdo = new PDO(DNS, USER_NAME, PASSWORD, get_pdo_options());

    $sql = "INSERT INTO users (name) values (?);";
    $stmt = $pdo->prepare($sql);
    // トランザクションの開始
    $pdo->beginTransaction();
    try {
      $stmt->bindValue(1, $name, PDO::PARAM_STR);
      $stmt->execute();
      // コミット
      $pdo->commit();
    } catch (PDOException $e) {
      // ロールバック
      $pdo->rollBack();
      throw $e;
    }

  } catch (PDOException $e) {
    die($e->getMessage());
  }

  //処理結果画面へ
  header('HTTP/1.1 301 Moved Permanently');
  header('Location: success.php');

 

・success.php

処理結果画面です。

<!DOCTYPE html>
<html lang="ja">
<body>
  <h1>登録しました</h1>
</body>
</html>

 

・error.php

エラー画面です。

<!DOCTYPE html>
<html lang="ja">
<body>
  <h1>処理が失敗しました</h1>
</body>
</html>

 

参考

Webアプリケーションセキュリティに関する記事は、以下のページにまとまっています。ぜひご確認ください。

更新履歴

・2020/08/18
トークンをセッションIDから乱数に変更。
データベースアクセスをMDB2からPDOに変更。

・2013/03/07
初版公開

 


スポンサーリンク





カテゴリー:Webアプリケーションセキュリティ対策

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

CAPTCHA