WordPress REST APIのセキュリティ完全ガイド|認証・権限チェック・Nonceの正しい実装

WordPressをヘッドレスCMS(Next.js / React / Vue / Nuxt等)として利用したり、モバイルアプリや外部システム、社内ツールと連携させたりする際、中核となるのがWordPress REST API(WP REST API)です。プラグイン開発やカスタムブロック(Gutenberg)の構築においても、非同期通信の基盤として欠かせない存在となっています。

しかし、REST APIは標準で外部インターネットに向けて広く開放されているため、認証(Authentication)権限チェック(Authorization / permission_callback)CSRF対策(Nonce)の設計を1つでも誤ると、重大なセキュリティインシデントに直結します。

実際、プラグインやカスタムテーマにおける脆弱性の多くは、「カスタムエンドポイントで権限チェックを忘れていた」「ログイン確認と編集権限の確認を混同していた」「未認証で機密情報(ユーザー情報や下書き記事)が取得可能になっていた」といったREST APIの実装不備に起因しています。

本記事では、WordPress REST APIを安全に設計・実装・運用するためのセキュリティ手法を網羅的に解説します。4大認証方式の使い分けから、X-WP-Nonce によるCSRF対策の実装手順、permission_callback の必須実装パターン、不要なエンドポイントの非公開化、レートリミット対策まで、プロダクション環境で今すぐ使える実践コードとともに完全ガイドします。


目次

WordPress REST APIのセキュリティリスクと基本概念

WordPress REST API(/wp-json/)は、WordPress 4.7でコアに統合されて以来、標準で有効化されています。特別な設定をしなくても外部からJSON形式で投稿や固定ページ、カテゴリなどのデータを取得できる強力な仕組みですが、その「初期状態でオープン」という性質がセキュリティ上の注意点となります。

REST APIで発生しやすい主なセキュリティ脅威

REST APIにおいて特に注意すべき代表的な脅威には以下のものがあります。

  • 認可制御の欠如(Broken Access Control / Missing Function Level Control): 最も頻発する脆弱性です。カスタムエンドポイントを追加した際に、リクエスト送信者が「本当にその操作を実行できる権限を持っているか」を検証していないケースです。
  • クロスサイトリクエストフォージェリ(CSRF): ブラウザのCookie認証を利用してREST APIを呼び出す際、悪意ある第三者サイトからの不正なリクエストを実行させられてしまう攻撃です。
  • ユーザー名・機密情報の露出(Information Disclosure): /wp-json/wp/v2/users エンドポイントへの未認証アクセスにより、管理者や投稿者のログインID・スラッグが収集され、ブルートフォース攻撃(パスワード総当たり)の足がかりにされるリスクです。
  • API経由のDoS攻撃・サーバー過負荷(Denial of Service): 重い集計処理や検索処理を行うエンドポイントに対して、制限なしに大量リクエストを送りつけられ、Webサーバーやデータベースがダウンするリスクです。

セキュリティ設計の3大基本原則(認証・認可・検証)

REST APIをセキュアに保つためには、次の3つのフェーズを明確に区別して実装する必要があります。

フェーズ役割と確認内容WordPressでの主な実装手段
① 認証(Authentication)リクエスト送信者が「誰であるか」を特定するCookie認証 / Application Passwords / JWT / OAuth
② 認可・権限(Authorization)特定されたユーザーが「その操作を行う権限があるか」を検証するpermission_callback 内での current_user_can()
③ 入力検証・無害化(Validation & Sanitization)送信されたデータが型・範囲を満たし安全かを検証・無害化するvalidate_callback / sanitize_callback

特によくある間違いが、「認証(ログインしているか)」と「認可(権限があるか)」の混同です。is_user_logged_in() は購読者(Subscriber)でも true を返すため、管理者向けの更新処理にこれだけを使用すると、一般会員ユーザーが悪意ある更新を行えてしまいます。必ず current_user_can() による厳密な権限チェックが必要です。


REST APIの認証方式まとめ(Cookie / Application Passwords / JWT / OAuth)

WordPress REST APIで利用できる認証方式には複数の選択肢があり、「クライアントがどこで動作しているか(ブラウザ同一オリジンか、外部サーバーか、モバイルアプリか)」によって最適な方式が異なります。

4大認証方式の比較表

認証方式通信元・環境必要ヘッダー / クレデンシャル特徴・メリット注意点・デメリット
Cookie認証 + Nonce同一オリジン(WordPressテーマ・プラグイン・管理画面内JS)ブラウザCookie + X-WP-NonceWordPress標準機能。ログインセッションをそのまま活用可能別ドメインやモバイルアプリ・CLIからは原則利用不可。CSRF対策必須
Application Passwords外部サーバー、CLIツール、テストスクリプトBasic認証ヘッダー(Authorization: Basic ...WordPress 5.6+標準機能。ユーザーごとに個別発行・即時失効可能HTTPS通信必須。ブラウザSPAからの直接利用には不向き(資格情報の露出リスク)
JWT認証(JSON Web Token)外部SPA(Next.js/Nuxt)、モバイルアプリ(iOS/Android)Bearerトークン(Authorization: Bearer <token>ステートレスでクロスドメイン通信が容易。モダンフロントエンドと親和性が高いプラグイン(例: JWT Authentication for WP REST API)導入が必要。署名鍵の秘匿とトークン失効管理が必要
OAuth 1.0a / OAuth 2.0サードパーティ製Webサービス・SaaS連携アクセストークン(Bearer <token>ユーザーにパスワードを知らせずに権限委譲が可能。業界標準規格実装・運用コストが高く、OAuthサーバー用プラグインが必要

各認証方式の詳細と使い分けガイド

1. Cookie認証(同一オリジンでのフロントエンド/管理画面向け):
ブラウザでWordPressにログインしている状態で、同一ドメイン内のJavaScript(管理画面プラグインUI、フロントエンドのAjax/Fetch通信、ブロックエディタ)からREST APIを叩く場合の標準方式です。ブラウザが自動送信するCookieによりユーザーが特定されますが、CSRF攻撃を防ぐために後述の X-WP-Nonce ヘッダーが必須となります。

2. Application Passwords(外部サーバー間・自動化バッチ向け):
WordPress 5.6からコア機能として追加されました。ユーザー管理画面から「アプリケーションパスワード」を個別に生成し、Basic認証として送信します。本物のログインパスワードを渡す必要がなく、用途ごとに名前をつけて発行・削除できるため、CI/CDパイプラインや外部システムからの定期データ同期に最適です。

3. JWT認証(ヘッドレスSPA・モバイルアプリ向け):
React/Next.jsなどの完全分離型フロントエンドやスマートフォンアプリからREST APIを利用する場合、Cookieによるクロスドメイン共有が難しいため、JWTトークンによる認証がデファクトスタンダードとなります。初回ログインAPIでトークンを取得し、以降のリクエストヘッダーに付与して通信します。


【実装手順】X-WP-Nonceヘッダーを使ったCSRF防御

Cookie認証を利用してREST APIを呼び出す場合、CSRF(Cross-Site Request Forgery:クロスサイトリクエストフォージェリ)の脆弱性を排除するために X-WP-Nonce ヘッダーの送信が不可欠です。

なぜREST APIでNonceが必要なのか?

ログイン済みのユーザーがブラウザで悪意ある外部Webページを開いた場合、そのページ内のJavaScriptからWordPress REST API(例: /wp-json/wp/v2/posts)に向けてPOSTリクエストが送信されると、ブラウザの仕様によりWordPressのログインCookieが自動的に付与されてしまいます。

もしCookieだけで認証を行っていると、攻撃者が仕組んだ不正な投稿作成や設定変更が実行されてしまいます。これを防ぐため、WordPressは「現在のユーザーのセッションとアクション名から生成された使い捨ての一時トークン(Nonce)」をリクエストヘッダーで要求し、正当な画面から送信されたリクエストであることを検証します。

WordPress REST API用のNonceは、アクション名 'wp_rest' で生成されます。

クライアント側(JavaScript/Fetch/Axios)からのNonce送信方法

PHP側で wp_create_nonce('wp_rest') を使ってNonceを発行し、JavaScript側に渡した上で、通信時に X-WP-Nonce ヘッダーを付与します。

ステップ1: PHP側でNonceとAPIエンドポイントをスクリプトに渡す

<?php
/**
 * スクリプトのエンキューとNonceの受け渡し
 */
add_action('wp_enqueue_scripts', 'my_enqueue_rest_scripts');
function my_enqueue_rest_scripts() {
    wp_enqueue_script(
        'my-custom-api-script',
        get_template_directory_uri() . '/js/api-client.js',
        array(),
        '1.0.0',
        true
    );

    // REST APIのURLとNonceをJavaScriptのグローバルオブジェクトとして渡す
    wp_localize_script(
        'my-custom-api-script',
        'wpApiSettings',
        array(
            'root'  => esc_url_raw(rest_url()),
            'nonce' => wp_create_nonce('wp_rest')
        )
    );
}

ステップ2-A: Fetch APIでのNonce送信コード例

/**
 * Fetch APIを用いたREST APIリクエスト(X-WP-Nonceヘッダー付与)
 */
async function createPostViaRest(title, content) {
    try {
        const response = await fetch(`${wpApiSettings.root}wp/v2/posts`, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                // Nonceヘッダーを必ず設定
                'X-WP-Nonce': wpApiSettings.nonce
            },
            body: JSON.stringify({
                title: title,
                content: content,
                status: 'draft'
            })
        });

        if (!response.ok) {
            const errorData = await response.json();
            throw new Error(errorData.message || `HTTP Error: ${response.status}`);
        }

        const data = await response.json();
        console.log('投稿作成成功:', data);
        return data;
    } catch (error) {
        console.error('API通信エラー:', error);
        throw error;
    }
}

ステップ2-B: Axiosでのグローバルインターセプター設定例

/**
 * Axiosで全リクエストにX-WP-Nonceを自動付与する設定
 */
import axios from 'axios';

const apiClient = axios.create({
    baseURL: wpApiSettings.root,
    headers: {
        'Content-Type': 'application/json'
    }
});

// リクエストインターセプターでNonceを自動付与
apiClient.interceptors.request.use((config) => {
    if (wpApiSettings.nonce) {
        config.headers['X-WP-Nonce'] = wpApiSettings.nonce;
    }
    return config;
}, (error) => {
    return Promise.reject(error);
});

// 使用例
export async function updateUserData(userId, payload) {
    const response = await apiClient.post(`myplugin/v1/user-settings/${userId}`, payload);
    return response.data;
}

サーバー側でのNonce自動検証の仕組み

WordPressコアは、REST APIリクエストを受信した際(rest_api_init 時点)、Cookieが存在している場合は自動的に X-WP-Nonce ヘッダー(またはクエリパラメータ _wpnonce)をチェックします。

  • Nonceが存在し、正しい場合: 現在のログインユーザーとして wp_get_current_user() が設定され、認証成功とみなされます。
  • Nonceが存在しない、または無効・期限切れの場合: Cookieが存在していてもユーザー認証は適用されず、未認証(匿名ユーザー)として処理されるか、rest_cookie_invalid_nonce エラー(HTTP 403 Forbidden)が返されます。
  • Nonceの有効期限: WordPressのNonceは通常12〜24時間有効です。長時間画面を開いたままにしているSPA等の場合、Nonceの期限切れによるエラーを防ぐため、定期的に新しいNonceを取得・再設定する仕組み(リフレッシュエンドポイント等)を設計することをおすすめします。

※WordPressのNonceの仕組みやAjax・フォームでの使い分けについてさらに詳しく知りたい方は、当ブログの「【WordPress】Nonce(ノンス)の使い方とCSRF対策完全ガイド」も併せてご覧ください。


カスタムエンドポイントでの権限チェック(permission_callback)の実装必須パターン

カスタムREST APIエンドポイントを追加する際、register_rest_route() 関数を使用します。WordPress 5.5以降、この関数で permission_callback の定義が必須(省略時は _doing_it_wrong 警告が発生) となりました。

しかし、警告を消すためだけに安易に 'permission_callback' => '__return_true' を設定してしまう開発者が後を絶ちません。これが多くのプラグイン脆弱性の温床となっています。

register_rest_route() の標準セキュアテンプレート

<?php
/**
 * セキュアなカスタムREST APIエンドポイントの登録
 */
add_action('rest_api_init', 'my_register_secure_routes');
function my_register_secure_routes() {
    register_rest_route('myplugin/v1', '/settings', array(
        'methods'             => WP_REST_Server::EDITABLE, // POST, PUT, PATCH
        'callback'            => 'my_update_plugin_settings',
        'permission_callback' => 'my_check_admin_permission',
        'args'                => array(
            'site_notice' => array(
                'required'          => true,
                'type'              => 'string',
                'validate_callback' => function($param, $request, $key) {
                    return is_string($param) && mb_strlen($param) <= 500;
                },
                'sanitize_callback' => 'sanitize_textarea_field',
            ),
            'enable_feature' => array(
                'required'          => false,
                'type'              => 'boolean',
                'sanitize_callback' => 'rest_sanitize_boolean',
            ),
        ),
    ));
}

パターン別:permission_callback の正しい実装例

パターン1: 管理者・特定権限(Capability)のチェック

サイト設定の更新など、特権操作を行うエンドポイントの権限チェックです。current_user_can() を用いて権限グループ(ロール)ではなく個別権限(Capability)を判定します。

<?php
/**
 * 管理者権限(manage_options)の検証
 * @param WP_REST_Request $request
 * @return bool|WP_Error
 */
function my_check_admin_permission($request) {
    if (!current_user_can('manage_options')) {
        return new WP_Error(
            'rest_forbidden',
            __('この設定を変更する権限がありません。', 'myplugin'),
            array('status' => rest_authorization_required_code()) // 401 または 403
        );
    }
    return true;
}

パターン2: 特定オブジェクト(投稿・個別データ)の編集権限チェック

リクエストされた特定の投稿IDに対して、現在のユーザーが編集権限を持っているかを検証します(Broken Object Level Authorizationの防止)。

<?php
/**
 * 特定の投稿に対する編集権限チェック
 * @param WP_REST_Request $request
 * @return bool|WP_Error
 */
function my_check_post_edit_permission($request) {
    $post_id = (int) $request->get_param('id');
    $post    = get_post($post_id);

    if (!$post) {
        return new WP_Error(
            'rest_post_invalid_id',
            __('指定された投稿が存在しません。', 'myplugin'),
            array('status' => 404)
        );
    }

    if (!current_user_can('edit_post', $post_id)) {
        return new WP_Error(
            'rest_cannot_edit',
            __('この投稿を編集する権限がありません。', 'myplugin'),
            array('status' => rest_authorization_required_code())
        );
    }

    return true;
}

パターン3: 一般公開(未認証許可)エンドポイントの明示的宣言

お問い合わせフォームの送信や公開記事の集計など、未認証の外部ユーザーからのアクセスを意図的に許可する場合は、'__return_true' を明示的に指定します。ただし、「書き込み系操作で公開にする場合は、必ずスパム対策やレートリミットを併用する」ことが鉄則です。

<?php
register_rest_route('myplugin/v1', '/public-inquiry', array(
    'methods'             => WP_REST_Server::CREATABLE, // POST
    'callback'            => 'my_handle_public_inquiry',
    'permission_callback' => '__return_true', // 意図的な公開エンドポイント
    'args'                => array(
        'email' => array(
            'required'          => true,
            'type'              => 'string',
            'validate_callback' => 'is_email',
            'sanitize_callback' => 'sanitize_email',
        ),
        'message' => array(
            'required'          => true,
            'type'              => 'string',
            'sanitize_callback' => 'sanitize_textarea_field',
        ),
    ),
));

引数バリデーション(validate_callback)と無害化(sanitize_callback)の徹底

権限チェックを通過した後の入力値についても、SQLインジェクションやXSS(クロスサイトスクリプティング)を防ぐためにパラメータ定義で型チェックとサニタイズを宣言します。

  • type: 'string', 'integer', 'boolean', 'array', 'object' などの基本データ型を指定。
  • validate_callback: 値が期待通りの形式(メール形式、許容文字数、配列構造など)を満たしているかを検証し、不適合なら false または WP_Error を返します。
  • sanitize_callback: 'sanitize_text_field''absint''rest_sanitize_boolean''wp_kses_post' などの無害化関数を指定します。

REST APIの不要なエンドポイント無効化・非公開化(ユーザー一覧露出の防止)

WordPressのデフォルト状態では、未認証の第三者でもブラウザから /wp-json/wp/v2/users にアクセスするだけで、登録されているユーザー名(ログインIDやスラッグ)の一覧が丸見えになってしまいます。

これにより、攻撃者に有効なアカウント名を特定され、パスワード総当たり攻撃(ブルートフォース攻撃)や標的型攻撃の成功率を大幅に高めてしまう危険性があります。

対策1: 未認証ユーザーに対するREST API全体アクセスの制限

サイトがヘッドレス構成などでなく、外部からのREST API利用が不要な通常サイトの場合は、ログイン済みユーザー以外へのREST APIアクセスを一括拒否するのが最も堅牢です。

<?php
/**
 * ログインユーザー以外へのREST APIアクセスをブロック
 */
add_filter('rest_authentication_errors', 'my_restrict_rest_api_to_logged_in_users');
function my_restrict_rest_api_to_logged_in_users($result) {
    // 既に他の認証エラーが発生している場合はそのまま返す
    if (!empty($result)) {
        return $result;
    }

    // ログインしていない場合はアクセス拒否エラーを返す
    if (!is_user_logged_in()) {
        return new WP_Error(
            'rest_not_logged_in',
            __('REST APIの利用にはログインが必要です。', 'myplugin'),
            array('status' => 401)
        );
    }

    return $result;
}

対策2: ユーザー一覧エンドポイント(/wp/v2/users)のみを非公開化する

公開記事の取得など通常のREST API利用は許可しつつ、ユーザー一覧(/wp/v2/users)のみを未認証ユーザーから隠蔽したい場合は、rest_endpoints フィルターを用いて対象ルートを削除します。

<?php
/**
 * 未認証ユーザーに対して /wp/v2/users エンドポイントを削除
 */
add_filter('rest_endpoints', 'my_disable_user_enumeration_endpoints');
function my_disable_user_enumeration_endpoints($endpoints) {
    // 未認証ユーザー(または管理者以外)の場合
    if (!current_user_can('list_users')) {
        if (isset($endpoints['/wp/v2/users'])) {
            unset($endpoints['/wp/v2/users']);
        }
        if (isset($endpoints['/wp/v2/users/(?P<id>[d]+)'])) {
            unset($endpoints['/wp/v2/users/(?P<id>[d]+)']);
        }
    }
    return $endpoints;
}

レートリミット(リクエスト制限)とWAFによるDoS対策

REST APIはプログラムから高速に連続リクエストを送信できるため、WebサーバーのCPU・メモリ枯渇やデータベース接続オーバーを引き起こすDoS攻撃(サービス妨害攻撃)の標的になりやすい特徴があります。

インフラ層での防御(Nginx / WAF)

最も負荷が低く効果的なのは、PHPプログラムが起動する前のWebサーバー(Nginx / Apache)やCDN/WAF(Cloudflare等)のレイヤーで流量制限(Rate Limiting)をかけることです。

Nginxでの /wp-json/ レート制限設定例

# /etc/nginx/nginx.conf または仮想ホスト設定
http {
    # ゾーン定義: 1IPあたり1秒間に5リクエストまで(メモリ10MB)
    limit_req_zone $binary_remote_addr zone=wp_rest_limit:10m rate=5r/s;

    server {
        # REST APIエンドポイントへのリクエスト制限
        location ~ ^/wp-json/ {
            # バースト許容20リクエスト、遅延なし
            limit_req zone=wp_rest_limit burst=20 nodelay;
            limit_req_status 429; # Too Many Requests を返却

            try_files $uri $uri/ /index.php?$args;
        }
    }
}

Cloudflare WAFでのレート制限ルール例

  • マッチ条件: URI Path starts_with "/wp-json/"
  • 制限値: 10秒間に30リクエスト超過時
  • アクション: 429レスポンス(Block)またはManaged Challenge(キャプチャ認証)を1分間適用

アプリケーション層(WordPress/PHP)での簡易レートリミット実装

サーバー設定を変更できない共有サーバー環境などでは、WordPressの rest_pre_dispatch フックと set_transient() を組み合わせて簡易的なIPベースのレートリミットを実装できます。

<?php
/**
 * REST APIのIPベースレートリミット(1分間に最大60リクエスト)
 */
add_filter('rest_pre_dispatch', 'my_rest_rate_limiter', 10, 3);
function my_rest_rate_limiter($result, $server, $request) {
    // ログイン中の管理者などは除外
    if (current_user_can('manage_options')) {
        return $result;
    }

    $ip = isset($_SERVER['REMOTE_ADDR']) ? sanitize_text_field($_SERVER['REMOTE_ADDR']) : '';
    if (empty($ip)) {
        return $result;
    }

    $transient_key = 'rest_rate_' . md5($ip);
    $request_count = (int) get_transient($transient_key);

    $limit = 60; // 1分間の上限リクエスト数

    if ($request_count >= $limit) {
        return new WP_Error(
            'rest_rate_limit_exceeded',
            __('リクエスト上限を超過しました。しばらく待ってから再試行してください。', 'myplugin'),
            array('status' => 429)
        );
    }

    // 初回は有効期限60秒でセット、2回目以降はカウントアップ
    if ($request_count === 0) {
        set_transient($transient_key, 1, 60);
    } else {
        // 残り時間は引き継ぎつつカウント更新(簡易加算)
        set_transient($transient_key, $request_count + 1, 60);
    }

    return $result;
}

WordPress REST API セキュリティ総括チェックリスト

WordPress REST APIを公開・本番運用する前に、以下のチェック項目を必ず確認してください。

カテゴリチェック項目合格基準・推奨対応
認証・CSRFCookie認証時に X-WP-Nonce を送信しているか?フロントエンド通信で wpApiSettings.nonce をヘッダー付与
認証・CSRF外部通信時に平文パスワードを直接渡していないか?Application Passwords または JWT Bearerトークンを使用
権限チェックカスタムエンドポイントに permission_callback を定義したか?省略せず、current_user_can() で厳密に権限判定
権限チェックオブジェクト固有の権限(個別Post ID等)を検証しているか?current_user_can('edit_post', $id) を実行
入力検証引数の型・形式バリデーションとサニタイズを行ったか?validate_callback / sanitize_callback を設定
情報漏洩対策未認証ユーザーへの /wp/v2/users 露出を遮断したか?rest_endpoints でユーザー一覧ルートを削除
DoS対策レートリミット(リクエスト制限)を設定したか?Nginx / WAF または Transient による 429 返却設定

まとめ:セキュアなREST API設計で堅牢なWordPressシステムを構築しよう

WordPress REST APIは、モダンなWebフロントエンドや外部システムと連携するための非常に優れた機能ですが、「APIエンドポイントは公開されたプログラムインターフェースである」という認識を持ってセキュリティ設計を行うことが不可欠です。

「認証」「認可(permission_callback)」「Nonce(CSRF対策)」「入力検証」「エンドポイント露出制限」「レートリミット」の多層防御を徹底することで、不正アクセスや改ざん、情報漏洩のリスクを極小化できます。


自社のWordPressサイトにセキュリティの抜け穴がないか無料診断してみませんか?

「自社サイトのREST API設定が安全か不安」「プラグインに未知の脆弱性が潜んでいないか確認したい」「攻撃者にユーザー名が漏洩していないかチェックしたい」というWeb担当者・サイト管理者の方は、当サイトが提供するWordPress専用セキュリティ診断ツール「MozCheck」の無料診断をぜひお試しください。

URLを入力するだけで、REST APIの露出状況やユーザー列挙リスク、プラグイン・テーマの脆弱性、SSL/セキュリティヘッダーの設定状態を数分で自動診断・可視化します。

👉 MozCheck無料診断の使い方と活かし方はこちら

投稿者

🧰 WordPress無料診断

サイト改善の第一歩をお届け

当サイト「MozCheck」は、WordPressサイトの不安をチェックできる無料診断サービスです。
URLを入力するだけ。登録不要、すぐに診断結果が表示されます。

コメント

コメントを残す

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