2026年10月13日

2026年10月13日

CSRFとXSS攻撃の対策方法(WordPressのコード例つき)

WordPress で CSRF と XSS を防ぐ基本は、状態を変える処理に「権限チェック+nonce の検証」を入れること、入力はサニタイズし、出力は出力先に合ったエスケープ関数で処理することの2点です。この記事では、2つの攻撃の違いと関係を整理したうえで、自作のテーマやプラグイン、functions.php のコードで実際に使う関数を、悪い例と良い例を対比しながら説明します。

CSRFとXSSの違いと、なぜ一緒に考えるのか

CSRF(クロスサイトリクエストフォージェリ) は、ログイン中の利用者に、本人の意図しないリクエストを送らせる攻撃です。たとえば管理者が別のサイトを開いた瞬間に、そのページに仕込まれたフォームが自動送信され、WordPress の設定が書き換えられる、といった形です。ブラウザはリクエストにログイン中の Cookie を自動で付けるため、サーバーは本人の操作と区別できません。

XSS(クロスサイトスクリプティング) は、攻撃者が用意したスクリプトを、対象サイトのページの中で実行させる攻撃です。投稿やコメント、URL のパラメータなどに含まれた文字列が、エスケープされずに HTML として出力されると起こります。データベースに保存されて表示のたびに実行される「格納型」、URL などの値がそのまま返される「反射型」、JavaScript が DOM を書き換える過程で起こる「DOM 型」があります。

この2つを一緒に考える理由は、XSS があると CSRF 対策が意味をなさなくなるからです。CSRF 対策の nonce はページの中に埋め込まれているため、同じページで攻撃者のスクリプトが動けば nonce を読み取ってリクエストを送れてしまいます。CSRF 対策と XSS 対策は、どちらか一方ではなく両方そろって初めて効果を発揮します。

CSRF対策: 権限チェックとnonceをセットで使う

WordPress の nonce は、特定のユーザーが特定の操作を、一定時間内に行うためのトークンです。既定では最長で約24時間有効で、ログインしているユーザーとセッションにひも付きます。名前は「一度だけ使える数値(number used once)」に由来しますが、WordPress の nonce は有効期間内なら何度でも使える点に注意してください。

もうひとつ大事なのは、nonce は「そのリクエストが自サイトの画面から送られたこと」を確かめるもので、「その人に操作する権限があること」は確かめない点です。権限は current_user_can() で別に確認します。

悪い例です。権限も nonce も確認せずに、送られてきた値で設定を更新しています。

// 悪い例: だれでも、どこからでも設定を書き換えられる
add_action( 'admin_init', function () {
    if ( isset( $_POST['myplugin_note'] ) ) {
        update_option( 'myplugin_note', $_POST['myplugin_note'] );
    }
} );

良い例です。フォームの出力側で wp_nonce_field() を使い、受け取る側で権限と nonce を確認します。

// フォームの出力(管理画面のページ内)
?>
<form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>">
    <input type="hidden" name="action" value="myplugin_save_note">
    <?php wp_nonce_field( 'myplugin_save_note', 'myplugin_nonce' ); ?>
    <input type="text" name="myplugin_note" value="<?php echo esc_attr( get_option( 'myplugin_note', '' ) ); ?>">
    <?php submit_button( '保存' ); ?>
</form>
<?php

// 受け取り側
add_action( 'admin_post_myplugin_save_note', 'myplugin_save_note' );
function myplugin_save_note() {
    if ( ! current_user_can( 'manage_options' ) ) {
        wp_die( esc_html__( 'この操作を行う権限がありません。', 'myplugin' ), '', array( 'response' => 403 ) );
    }
    check_admin_referer( 'myplugin_save_note', 'myplugin_nonce' );

    $note = isset( $_POST['myplugin_note'] )
        ? sanitize_text_field( wp_unslash( $_POST['myplugin_note'] ) )
        : '';
    update_option( 'myplugin_note', $note );

    wp_safe_redirect( admin_url( 'options-general.php?page=myplugin' ) );
    exit;
}

check_admin_referer() は nonce が無効なら処理を止めます。$_POST の値は WordPress によってスラッシュが付加されているため、wp_unslash() で戻してからサニタイズします。

Ajax(admin-ajax.php)では wp_create_nonce() で作った値を JavaScript に渡し、受け取り側で check_ajax_referer( 'myplugin_ajax', 'nonce' ) を使います。REST API で独自のエンドポイントを作る場合は、register_rest_route() の permission_callback に権限チェックを書きます。ログイン中のユーザーとして JavaScript から REST API を呼ぶときは、wp_create_nonce( 'wp_rest' ) の値を X-WP-Nonce ヘッダーで送ると、Cookie 認証のリクエストとして扱われます。permission_callback に __return_true を指定すると誰でも呼び出せるエンドポイントになるため、公開してよい読み取り専用のものに限ってください。

ブラウザ側の対策として、Cookie の SameSite 属性も CSRF を起こりにくくします。多くのブラウザは属性が指定されていない Cookie を Lax として扱いますが、ブラウザによって挙動が異なるため、SameSite だけに頼らず nonce による検証を基本にします。

XSS対策: 入力はサニタイズ、出力はエスケープ

XSS を防ぐ中心は出力時のエスケープです。保存前のサニタイズも大事ですが、データはインポートや他のプラグインなど、自分のコードを通らない経路でも入ってくるため、「保存時に処理したから表示時は不要」とは考えないでください。WordPress のコーディング規約でも、出力の直前でエスケープする「遅いエスケープ」が推奨されています。

エスケープ関数は、出力する場所に合わせて選びます。

  • HTML の本文として出す: esc_html()
  • HTML の属性値に入れる: esc_attr()
  • href や src に URL を入れる: esc_url()(javascript: などの危険なスキームを取り除きます)
  • テキストエリアの中に入れる: esc_textarea()
  • 一部の HTML タグを許可したい: wp_kses_post() や、許可するタグを指定した wp_kses()

悪い例と良い例を並べます。

// 悪い例: 検索語や投稿メタをそのまま出力している
echo '<p>検索語: ' . $_GET['q'] . '</p>';
echo '<a href="' . get_post_meta( $post->ID, 'link', true ) . '">リンク</a>';

// 良い例: 出力先に合わせてエスケープする
$q = isset( $_GET['q'] ) ? sanitize_text_field( wp_unslash( $_GET['q'] ) ) : '';
echo '<p>検索語: ' . esc_html( $q ) . '</p>';
echo '<a href="' . esc_url( get_post_meta( $post->ID, 'link', true ) ) . '">リンク</a>';

入力側では、値の種類に合ったサニタイズ関数を使います。1行のテキストは sanitize_text_field()、複数行は sanitize_textarea_field()、メールアドレスは sanitize_email()、整数は absint()、キーやスラッグは sanitize_key() が代表的です。選択肢から選ぶ値は、許可した値の一覧と照合する方が確かです。

JavaScriptで起こるXSSを防ぐ

PHP 側でエスケープしていても、JavaScript で受け取った値を innerHTML で差し込むと DOM 型の XSS が起こります。文字列として表示するなら textContent を使います。

// 悪い例
listItem.innerHTML = data.title;

// 良い例
listItem.textContent = data.title;

PHP から JavaScript に値を渡すときも、文字列を連結して <script> を組み立てるのではなく、wp_add_inline_script() と wp_json_encode() を組み合わせるか、wp_localize_script() を使います。

wp_enqueue_script( 'myplugin-admin', plugins_url( 'admin.js', __FILE__ ), array(), '1.0.0', true );
wp_add_inline_script(
    'myplugin-admin',
    'const MyPlugin = ' . wp_json_encode( array(
        'ajaxUrl' => admin_url( 'admin-ajax.php' ),
        'nonce'   => wp_create_nonce( 'myplugin_ajax' ),
    ) ) . ';',
    'before'
);

運用面での対策

自分のコード以外にも、次の点を確認しておくと被害の可能性を下げられます。

  • unfiltered_html 権限: シングルサイトでは管理者と編集者が、投稿にスクリプトを含む HTML を書ける権限を持っています。アカウントが乗っ取られると格納型 XSS の入り口になるため、編集者の人数を絞り、強いパスワードと二要素認証を使います。wp-config.php に define( 'DISALLOW_UNFILTERED_HTML', true ); を書いて、この権限を外すこともできます
  • プラグインとテーマの更新: 公開されている XSS や CSRF の脆弱性の多くは、プラグインやテーマで見つかっています。更新を止めているものや、配布が終了したものがないか定期的に確認します
  • Content-Security-Policy: 実行を許可するスクリプトの読み込み元を HTTP ヘッダーで制限でき、XSS が起きた場合の被害を減らせます。ただし WordPress のコアや多くのプラグインはインラインスクリプトを使うため、厳しい設定はそのままでは管理画面などが動かなくなります。まず Content-Security-Policy-Report-Only で違反を記録し、影響を見ながら調整してください
  • WAF: Wordfence のようなセキュリティプラグインや、クラウド型の WAF は既知の攻撃パターンを止める助けになりますが、コード側の対策の代わりにはなりません

テストは自分が管理するサイト、できればステージング環境で行い、他人のサイトに攻撃用の文字列を送らないでください。WordPress のバージョンや関数の仕様は変わることがあるため、使う関数は WordPress の開発者向けドキュメントで確認してください(2026年10月現在)。

まとめ

  • CSRF は「意図しないリクエストを送らせる」、XSS は「スクリプトを実行させる」攻撃で、XSS があると nonce による CSRF 対策も破られる
  • 状態を変える処理には current_user_can() と nonce の検証(check_admin_referer()、check_ajax_referer()、REST の permission_callback)をセットで入れる
  • 入力は sanitize_text_field() などでサニタイズし、出力は esc_html()、esc_attr()、esc_url() などを出力の直前で使う
  • JavaScript では innerHTML ではなく textContent を使い、値は wp_json_encode() で渡す
  • 編集権限の管理、プラグインの更新、CSP や WAF は補助として組み合わせる

お気軽にご相談ください

お見積りへ お問い合わせへ
▲