current_user_can()によるWordPress権限チェック実践|脆弱性を防ぐ認可・Capability設計

WordPressプラグインやテーマの開発において、最も頻繁に発見されるセキュリティ脆弱性のひとつが「Missing Authorization(認可チェックの欠如・不備)」です。

「管理画面(wp-admin)からしか送信できないフォームだから安全」「URLを知らなければ叩かれない」という思い込みは極めて危険です。一般購読者(Subscriber)や悪意ある攻撃者が直接リクエストを送信した場合、適切な認可チェックがなければ、設定の改ざんや管理者権限の奪取、非公開データの閲覧を許してしまいます。

💡 本記事は「WordPress セキュアコーディング実践シリーズ」の第3弾です

本記事では、WordPressの認可制御の基本関数 current_user_can() の正しい使い方から、やってしまいがちなアンチパターン(is_admin()の誤用など)、メタCapabilityによる投稿単位の厳格制御、そしてカスタムCapabilityの設計までを実践的に解説します。


1. なぜ「is_admin()」やロール名直接チェックは危険なのか?

WordPress開発で最も多い致命的な勘違いが、is_admin() の誤用です。

【危険】is_admin() は「権限チェック関数」ではない

is_admin() は単に「現在リクエストされているページが /wp-admin/ 配下のURLかどうか」を判定するだけのUI判定関数です。

  • 一般購読者(Subscriber)が管理画面のダッシュボード(プロファイル画面等)を開いている時も is_admin() === true になります。
  • さらに、Ajax処理のエンドポイント /wp-admin/admin-ajax.php へのリクエストは、ログインしていない未認証ゲストからのリクエストであっても is_admin() === true になります。
// ❌ 致命的な脆弱性コード(未ログインでもSubscriberでも通過してしまう)
if ( is_admin() ) {
    delete_option('my_plugin_important_settings');
}

WordPressの原則:Role(役割名)ではなく Capability(権限)で判定する

WordPressでは「管理者(administrator)かどうか」をロール名で直接判定するのではなく、「設定変更できる権限(manage_options)を持っているか」というCapability(ケーパビリティ)で判定するのが大原則です。

// ⭕ 正しいセキュアな判定
if ( current_user_can('manage_options') ) {
    delete_option('my_plugin_important_settings');
}

2. current_user_can() の基本と代表的なCapability一覧

current_user_can( string $capability, mixed ...$args ) は、現在アクセス中のログインユーザーが指定した権限(Capability)を持っているかどうかを真偽値(true / false)で返します。

Capability(権限名)標準で保持するロール主な利用シーン
manage_options管理者(Administrator)プラグイン設定、サイト全般の重要設定の保存・変更
edit_posts寄稿者以上(Contributor+)自身の投稿の作成・下書き保存
publish_posts投稿者以上(Author+)投稿の公開
edit_others_posts編集者以上(Editor+)他ユーザーが書いた投稿の編集
upload_files投稿者以上(Author+)メディア(画像・ファイル)のアップロード
read購読者以上(Subscriber+)管理画面のプロフィール閲覧・ログイン状態の確認

3. メタCapability(Meta Capabilities)による個別リソース認可

ブログ投稿やカスタム投稿タイプの編集・削除処理を行う場合、edit_posts(投稿を作成できるか)だけをチェックすると、「寄稿者Aが寄稿者Bの記事を勝手に書き換えてしまう」という権限昇格・越境操作の脆弱性が発生します。

WordPressには、対象リソースのIDを渡すことで「そのユーザーが“その特定の投稿”を編集できるか」を文脈に応じて動的に判定するメタCapability(Meta Capabilities)が備わっています。

メタCapabilityの実装コード例

<?php
// 投稿IDを指定して個別編集権限をチェック
$post_id = isset($_POST['post_id']) ? absint($_POST['post_id']) : 0;

if ( ! current_user_can('edit_post', $post_id) ) {
    wp_die(
        esc_html__('あなたにはこの記事を編集する権限がありません。', 'my-plugin'),
        esc_html__('権限エラー', 'my-plugin'),
        array('response' => 403)
    );
}

// 権限チェック通過後に更新処理を実行
wp_update_post(array(
    'ID'           => $post_id,
    'post_content' => wp_kses_post($_POST['custom_content']),
));

current_user_can('edit_post', $post_id) は、内部で map_meta_cap() を呼び出し、ユーザーがその投稿の作成者であるか、あるいは edit_others_posts 権限を持つ編集者以上であるかを自動判定してくれます。


4. 開発シーン別:セキュアな権限チェックの実装パターン

パターン1: 管理画面のPOST処理(admin-post.php)

管理画面フォームの送信処理では、nonce検証と権限チェックの「2重防御(Defense in Depth)」が必須です。

<?php
add_action('admin_post_my_save_action', 'my_handle_admin_save');

function my_handle_admin_save() {
    // 1. nonceの検証(CSRF対策)
    check_admin_referer('my_save_action_nonce', 'my_nonce_field');

    // 2. 権限チェック(認可チェック)
    if ( ! current_user_can('manage_options') ) {
        wp_die(
            esc_html__('この設定を変更する権限がありません。', 'my-plugin'),
            403
        );
    }

    // 3. サニタイズして保存
    $input_value = isset($_POST['my_setting']) ? sanitize_text_field($_POST['my_setting']) : '';
    update_option('my_plugin_setting_key', $input_value);

    // リダイレクト
    wp_safe_redirect(add_query_arg('updated', 'true', wp_get_referer()));
    exit;
}

パターン2: Ajax処理(wp_ajax_*)での権限チェック

<?php
add_action('wp_ajax_my_delete_item', 'my_handle_ajax_delete');

function my_handle_ajax_delete() {
    // 1. nonce検証
    check_ajax_referer('my_ajax_security_nonce', 'security');

    // 2. 権限チェック
    if ( ! current_user_can('manage_options') ) {
        wp_send_json_error(array('message' => '権限がありません。'), 403);
    }

    $item_id = isset($_POST['item_id']) ? absint($_POST['item_id']) : 0;
    // 削除処理の実行...

    wp_send_json_success(array('message' => '正常に削除されました。'));
}

5. カスタムCapabilityの設計と追加手順

プラグイン専用の高度な機能を提供する場合、manage_options(全権限を持つ管理者のみ)に縛られるのではなく、独自のCapability(例: manage_my_reportsを定義して特定のロールに付与するのが理想的な設計です。

<?php
// プラグイン有効化フックで一度だけ実行
register_activation_hook(__FILE__, 'my_plugin_activate_capabilities');

function my_plugin_activate_capabilities() {
    // 管理者ロールを取得してカスタムCapabilityを追加
    $admin_role = get_role('administrator');
    if ($admin_role) {
        $admin_role->add_cap('manage_my_custom_reports');
    }

    // 編集者ロールにもレポート閲覧権限を付与
    $editor_role = get_role('editor');
    if ($editor_role) {
        $editor_role->add_cap('view_my_custom_reports');
    }
}

6. 権限チェック実装のセキュリティ5箇条

  1. is_admin()を権限判定に使わない:表示環境判定であり、認可には必ず current_user_can() を用いる。
  2. nonce検証と権限チェックを両方行う:nonce(CSRF防止)だけでは認可不足、権限チェックだけでもCSRFに無防備になる。
  3. 個別リソースにはメタCapabilityを使う:投稿やユーザーの編集・削除には edit_postedit_user にIDを添えて判定する。
  4. 最小権限の原則(Least Privilege):何でも manage_options にせず、適切な粒度のCapabilityを割り当てる。
  5. 早期リターン&適切なHTTPステータス:権限不足時は即座に処理を中断し、403 Forbidden を返却する。

7. まとめ&セキュアコーディングシリーズ関連記事

WordPressにおける権限チェックは、アプリケーションの安全性を担保する最も基本的な防壁です。current_user_can() を正しく理解し、Capabilityベースで設計することで、認可不備による情報漏えいや不正操作のリスクを根絶できます。

📚 開発者向けセキュアコーディング関連記事

🛡️ WordPressサイトの権限設定やセキュリティ状態をチェック!

プラグインの認可不備やセキュリティホールは、外部からの攻撃に直結します。「MozCheck」なら、URLを入力するだけでサイト全体のセキュリティ状態を自動診断できます。

投稿者

🧰 WordPress無料診断

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

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

コメント

コメントを残す

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