1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

CSRF対策をPHP・WordPressで実装する手順(トークン検証・SameSite・コピペOK)

1
Posted at

フォームやAPIに「ログインしているユーザーになりすまして操作を送りつけられる」CSRF(クロスサイトリクエストフォージェリ)の対策を、そのままコピペして使える形でまとめました。

  • 対象: 自作フォーム・管理画面・お問い合わせ機能を PHP や WordPress で運用している Web 担当者・制作会社の方
  • 解決すること: 退会・パスワード変更・投稿削除・設定変更などの重要操作が、他サイトから勝手に実行されるのを防ぐ
  • 前提知識: HTML フォームと PHP の基本(専門用語には都度補足を付けます)

SQLインジェクションや XSS と並ぶ、Web アプリで作り込みやすい古典的な穴です。仕組みを1分で押さえたうえで、実装に入ります。

CSRF とは何か(1分で)

CSRF は、利用者のブラウザを踏み台にして、ログイン済みの状態を悪用する攻撃です。

ポイントは「Cookie が自動で送られる」というブラウザの仕様にあります。利用者が自分のサイト(例: example.com)にログインしていると、ブラウザはそのサイト宛のリクエストにセッション Cookie を自動で付けます。ここで利用者が別の悪意あるページを開き、そのページが裏で example.com に対してフォーム送信を行うと、利用者本人のセッション Cookie が付いたままリクエストが飛んでしまいます。サーバー側は「ログイン済みの本人からの正当な操作」と誤認して処理してしまう、というのが CSRF の本質です。

つまり CSRF は「入力値の中身」ではなく「そのリクエストは、本当に自サイトの画面から送られたものか?」を検証できていないことが原因です。ここでは攻撃の再現手順には踏み込まず、同じ被害を防ぐ側の実装だけを扱います。

対策の全体像:3つの柱

CSRF 対策は次の3点を組み合わせるのが定石です。1つだけでは穴が残ります。

  1. CSRFトークンの検証 — 自サイトの画面が発行した「合言葉」を、フォーム送信時に照合する(中心的な対策)
  2. SameSite Cookie — 他サイト起点のリクエストにセッション Cookie を付けさせない(ブラウザ側の防御)
  3. 安全な設計 — 状態を変える操作は必ず POST(GET でやらない)、必要なら Origin/Referer も補助的に確認

順に実装していきます。

1. PHP の素の実装:CSRFトークン

セッション開始時にランダムなトークンを1つ生成し、セッションに保存します。random_bytes() で暗号論的に安全な乱数を使うのが必須です(rand()uniqid() は推測されうるため不可)。

<?php
session_start();

// トークンが未発行ならセッションに1つ生成して保存
if (empty($_SESSION['csrf_token'])) {
    $_SESSION['csrf_token'] = bin2hex(random_bytes(32)); // 64文字の16進文字列
}
$csrf_token = $_SESSION['csrf_token'];

フォーム側では、このトークンを hidden フィールドに埋め込みます。表示時は必ず htmlspecialchars() でエスケープします。

<form action="/update.php" method="post">
  <input type="hidden" name="csrf_token"
         value="<?php echo htmlspecialchars($csrf_token, ENT_QUOTES, 'UTF-8'); ?>">
  <!-- 本来のフォーム項目 -->
  <button type="submit">更新する</button>
</form>

受け取り側(POST を処理する update.php)では、送られてきたトークンとセッションのトークンを照合します。比較には必ず hash_equals() を使い、タイミング攻撃を避けます(===== での文字列比較は使わない)。

<?php
session_start();

// POST 以外で状態を変えない(GET での更新は禁止)
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
    http_response_code(405);
    exit('Method Not Allowed');
}

$posted = $_POST['csrf_token'] ?? '';
if (empty($_SESSION['csrf_token']) || !hash_equals($_SESSION['csrf_token'], $posted)) {
    http_response_code(403);
    exit('CSRF token mismatch'); // 本番では詳細をログにのみ残す
}

// ここまで通れば正当な送信。実際の更新処理へ

トークンはセッション単位で使い回して問題ありません(1リクエストごとに使い捨てる必要はなく、使い捨てにするとタブ複数開き・戻る操作で誤検知が増えます)。ログアウト時にセッションごと破棄されれば十分です。

2. SameSite Cookie でブラウザ側からも防ぐ

SameSite 属性は、他サイト起点のリクエストにセッション Cookie を付けるかどうかをブラウザに指示するものです。Lax にすると、他サイトからの POST 送信には Cookie が付かなくなり、CSRF の大部分を土台から無効化できます。

PHP のセッション Cookie に付ける場合は、session_start()前にパラメータを設定します。

<?php
session_set_cookie_params([
    'lifetime' => 0,
    'path'     => '/',
    'secure'   => true,   // HTTPS 必須(HTTP では送らせない)
    'httponly' => true,   // JavaScript から読めなくする
    'samesite' => 'Lax',  // 他サイト起点の POST に付けない
]);
session_start();

php.ini でサイト全体に効かせる場合はこちら。

session.cookie_secure   = 1
session.cookie_httponly = 1
session.cookie_samesite = Lax

アプリを直接触れず、リバースプロキシで一律に付与したい場合の例も挙げておきます。nginx(1.19.3 以降)は proxy_cookie_flags が使えます。

# upstream が返す Set-Cookie に属性を追記
proxy_cookie_flags ~ secure samesite=lax;

Apache(mod_headers)なら次のように書けます。

Header always edit Set-Cookie ^(.*)$ "$1; SameSite=Lax; Secure"

Strict ではなく Lax を既定にするのが安全側の推奨です。Strict は外部リンクから遷移した直後のリクエストにも Cookie を付けないため、「メールのリンクを踏んだらログアウト状態に見える」といった正規利用の不便を生みがちです。SameSite=None は Cookie をクロスサイトで送る設定なので、CSRF 対策としては選びません(どうしても必要な決済連携などに限定)。

SameSite の詳しい挙動は MDN のリファレンス(https://developer.mozilla.org/ja/docs/Web/HTTP/Headers/Set-Cookie/SameSite )が正確です。ただし SameSite は古いブラウザや例外的な経路で効かないこともあるため、トークン検証との二段構えが前提です。

3. WordPress での実装:nonce を使う

WordPress を使っている場合は、自前でトークンを作らず**nonce(number used once の略。用途と時間で限定した合言葉)**の仕組みを使います。公式のセキュリティ API(https://developer.wordpress.org/apis/security/nonces/ )に沿って実装します。

フォームには wp_nonce_field() で hidden フィールドを出力します。第1引数は「操作を表す一意な文字列(action)」です。

<?php
// フォーム内で nonce フィールドを出力(action は操作ごとに変える)
wp_nonce_field('delete_post_' . $post_id, 'my_nonce');
?>
<!-- 送信ボタンなど -->

受け取り側では check_admin_referer() で検証します。不正なら WordPress が処理を止めてくれます。

<?php
// 管理画面のフォーム送信を処理する箇所
check_admin_referer('delete_post_' . $post_id, 'my_nonce');

// 権限も必ず併せて確認する(nonce は「なりすまし防止」であって権限確認ではない)
if (!current_user_can('delete_post', $post_id)) {
    wp_die('権限がありません');
}
// ここから実処理

Ajax や REST API 経由の場合は、それぞれ専用の検証関数を使います。

<?php
// 管理画面 Ajax(admin-ajax.php)ハンドラの冒頭
check_ajax_referer('my_ajax_action', 'nonce');

// REST API はリクエストヘッダ X-WP-Nonce を wp_create_nonce('wp_rest') で発行し、
// register_rest_route の permission_callback で権限を確認する

WordPress の nonce は1回限りの使い捨てではなく、action とユーザー・セッションに紐づいた時間制限つき(既定で最長24時間)のトークンです。厳密なワンタイムトークンではない点は理解しておきましょう。それでも「その操作画面を実際に開いた本人からの送信か」を十分に担保できます。

4. 補助:Origin / Referer の確認

トークン検証が主役ですが、重要操作では Origin ヘッダの確認を補助的に足すと堅くなります。Origin は多くのブラウザが自動で付け、偽装が難しいためです。

<?php
$origin  = $_SERVER['HTTP_ORIGIN'] ?? '';
$allowed = 'https://example.com'; // 自サイトのオリジンに置き換える

// Origin があるのに自サイトと一致しないときだけ弾く
if ($origin !== '' && $origin !== $allowed) {
    http_response_code(403);
    exit('Origin mismatch');
}

Referer は環境によって送られないことがあるため、**「無ければ通す」ではなく「あるなら一致を確認する」**という補助的な使い方に留めます。これ単独を主対策にはしません。

よくある間違い(ここを外すと無効化する)

  • GET で状態を変える: ?action=delete&id=5 のような GET リクエストで削除・更新をしている。GET は他サイトから画像タグ1つで発火でき、SameSite=Lax でも一部通るため、状態変更は必ず POST にする。
  • トークンを URL に載せる: クエリ文字列に付けると Referer やアクセスログ経由で漏れる。必ず hidden フィールド(またはリクエストヘッダ)で送る。
  • 検証を一部の画面で忘れる: 新しく追加したフォームや API だけ検証が抜けがち。状態を変える入口を一覧化し、全経路に適用する。
  • 比較に == を使う: PHP の緩い比較は型変換で予期せぬ一致を起こす。照合は hash_equals() で行う。
  • nonce を権限確認の代わりにする: nonce は「本人の画面からの送信か」を確かめるだけ。「その人にその操作の権限があるか」は current_user_can() で別途必ず確認する。

まとめ

  • CSRF は入力値ではなく「そのリクエストが自サイトの画面由来か」を検証できていないことが原因。
  • 対策は ①トークン検証(random_bytes + hash_equals)②SameSite=Lax Cookie ③状態変更は POST + 補助的な Origin 確認の組み合わせ。
  • WordPress は自作せず **nonce API(wp_nonce_field / check_admin_referer / check_ajax_referer / X-WP-Nonce)**を使い、権限確認と必ずセットにする。

XSS でトークンを盗まれると CSRF 対策も崩れるため、出力エスケープや Cookie 属性とあわせた多層防御が前提です。関連記事もあわせてどうぞ。

関連記事

本記事のような実装の抜けは、Web サイトを9つの守りでまるごと守るサイトドックでまとめて対策できます → https://sitedock.jp

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?