mcpField
W
wp-security-review

wp-security-review

github.com/jorgerosalセキュリティ111117 回インストール20 週間前に更新
リポジトリ

wp-security-review について

WordPress のテーマ、プラグイン、カスタムコードを対象とするセキュリティレビュー用スキルです。入力処理、認可、出力エスケープを確認し、深刻度、行番号、CWE、修正例を添えて報告します。

主な機能

  1. 01入力のサニタイズ、認可、出力エスケープを確認
  2. 02SQL インジェクション、XSS、CSRF を調査
  3. 03REST API、AJAX、アップロード処理を確認
  4. 04ファイルと深刻度別に指摘を整理
  5. 05行番号、CWE、修正コードを提示

活用シーン

  1. 01公開前の WordPress プラグインレビュー
  2. 02テーマやプラグインの脆弱性報告の調査
  3. 03フォーム、AJAX、REST API ハンドラーの監査

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/jorgerosal/wordpress-skills --skill wp-security-review

インストール後、Claude Code で/wp-security-reviewを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

references/
auth-patterns.md
escaping-guide.md
nonce-csrf-guide.md
sanitization-guide.md
vulnerability-patterns.md

詳細

WordPress セキュリティレビューのスキル

概要

WordPress のテーマ、プラグイン、カスタムコードを対象とした体系的なセキュリティコードレビューです。中核となる原則: 3 つの柱からなるセキュリティモデルです。(1)入力を早い段階でサニタイズする、(2)認可を検証する(ノンス+権限)、(3)出力を遅い段階でエスケープする。まず重大な問題(公開コードにおける SQL インジェクション、エスケープされていない出力での XSS、状態を変更する操作でのノンスの欠落)をスキャンし、次に警告、最後に情報レベルの改善点を確認します。行番号、重大度、CWE 参照、BAD/GOOD のコード例を添えて報告します。

使用する場面

次の場合に使用します:

  • WordPress のテーマまたはプラグインのセキュリティ監査
  • リリース前のセキュリティレビュー
  • 報告された脆弱性の調査
  • フォームハンドラー、AJAX エンドポイント、REST API ルートのレビュー
  • ユーザー入力の処理や出力のレンダリングの分析
  • 悪用可能なパターン(XSS、SQL インジェクション、CSRF、認証バイパス)のコードレビュー

次の場合には使用しないでください:

  • サーバーの強化やインフラストラクチャのセキュリティ(Apache/nginx の設定、ファイアウォールルール)
  • Web アプリケーションファイアウォール(WAF)の設定
  • SSL/TLS 証明書の管理
  • パフォーマンスのみを対象とする監査(wp-performance-review を使用してください)
  • WordPress 以外の一般的な PHP セキュリティ

コードレビューワークフロー

  1. ファイルの種類を特定し、以下の該当するチェックを適用します
  2. 重大なパターンを最初にスキャンします(公開コードにおける SQL インジェクション、エスケープされていない出力での XSS、状態を変更する操作でのノンスの欠落、検証なしのファイルアップロード)
  3. 警告パターンを確認します(認可の問題、管理者専用の XSS、不完全な検証)
  4. 情報レベルの改善点を記録します(多層防御、セキュリティ定数、強化の機会)
  5. コンテキストに応じて重大度を調整します(管理者専用コード=重大度を下げる、公開コード=最高の重大度、WP-CLI/cron=ノンス不要、REST API=異なる認証モデル)
  6. 以下の出力形式を使用して、行番号付きで報告します

ファイルの種類別チェック

プラグイン/テーマの PHP ファイル(functions.php、plugin.php、*.php)

以下をスキャンします:

  • ファイルの先頭に defined( 'ABSPATH' ) || exit; がない → WARNING:ファイルへ直接アクセスできる可能性があります
  • eval( → CRITICAL:コードインジェクションの可能性(CWE-95)
  • exec(、shell_exec(、system(、passthru( → CRITICAL:ユーザー入力が存在する場合、コマンドインジェクションの可能性(CWE-78)
  • base64_decode( $_ → CRITICAL:エンコードされたペイロードが実行される可能性があります
  • unserialize( $_ → CRITICAL:オブジェクトインジェクション(CWE-502)
  • sanitize_* なしでの $_GET[、$_POST[、$_REQUEST[ → WARNING:サニタイズされていない入力(CWE-20)
  • include $_、require $_、include_once $_、require_once $_ → CRITICAL:パストラバーサル/インクルージョン(CWE-22)
  • 生の move_uploaded_file( → CRITICAL:代わりに wp_handle_upload() を使用してください
フォームハンドラー(admin-post.php、admin_post_* フック)

次の 3 ステップのパターンをスキャンします(すべて必須):

  1. ノンス検証 → wp_verify_nonce( $_POST['nonce_field'], 'action_name' ) または check_admin_referer() → 欠落している場合は CRITICAL(CWE-352)
  2. 権限チェック → current_user_can( 'capability' ) → 欠落している場合は CRITICAL(CWE-862)
  3. 入力のサニタイズ → sanitize_text_field()、sanitize_email() など → 欠落している場合は WARNING(CWE-20)

いずれかのステップが欠落している場合は、脆弱性です。状態を変更する操作には、3 つすべてが必須です。

AJAX ハンドラー(wp_ajax_*、wp_ajax_nopriv_* フック)

以下をスキャンします:

  • check_ajax_referer( 'action_name', 'nonce_field' ) → 状態を変更する操作で欠落している場合は CRITICAL(CWE-352)
  • current_user_can( 'capability' ) → 権限が必要な箇所で欠落している場合は CRITICAL(CWE-862)
  • ノンスのない wp_ajax_nopriv_* → CRITICAL:CSRF 保護のない公開エンドポイント
  • sanitize_* なしでの $_POST からの入力 → WARNING:サニタイズされていない入力(CWE-20)
  • ユーザー入力を出力する際にエスケープを行わない wp_send_json() → WARNING:JSON インジェクション
REST API エンドポイント(register_rest_route)

以下をスキャンします:

  • permission_callback の欠落 → CRITICAL:WordPress 5.5 以降では必須です(CWE-862)
  • 書き込み操作での 'permission_callback' => '__return_true' → CRITICAL:不正アクセスを許してしまいます
  • 公開読み取りエンドポイントでの 'permission_callback' => '__return_true' → OK(脆弱性ではありません)
  • permission callback に current_user_can() がない → WARNING:権限昇格を許す可能性があります(CWE-863)
  • $request->get_param() に対する入力検証がない → WARNING:入力がサニタイズされていません(CWE-20)
  • X-WP-Nonce ヘッダーまたはアプリケーションパスワードを介した REST 認証 → INFO:クライアントがヘッダーを送信していることを確認してください
データベースコード($wpdb->query、$wpdb->get_results など)

以下をスキャンします:

  • 文字列連結または文字列補間を使用する $wpdb->query( または $wpdb->get_results( → CRITICAL:SQL インジェクション(CWE-89)
  • ユーザー入力がある場合に $wpdb->prepare() がない → CRITICAL:SQL インジェクション(CWE-89)
  • $wpdb->prepare( "... LIKE '%{$term}%'" ) → WARNING:最初に $wpdb->esc_like() を使用してください
  • 二重の prepare(すでに prepare された文字列を再度 prepare する)→ WARNING:エスケープ文字がエスケープされます
  • クエリ内での {$wpdb->prefix} の直接使用 → OK(脆弱性ではなく、標準的なパターンです)

安全なパターン:$wpdb->get_results( $wpdb->prepare( "SELECT * FROM {$wpdb->posts} WHERE ID = %d", $post_id ) )

テンプレート/出力ファイル(テーマ、テンプレート内の *.php)

以下をスキャンします:

  • エスケープ関数を使わない echo $ または print $ → CRITICAL:XSS(CWE-79)
  • <input value="<?php echo $ → CRITICAL:esc_attr() を使用してください(CWE-79)
  • <a href="<?php echo $ → CRITICAL:esc_url() を使用してください(CWE-79)
  • PHP 変数を含む <script> ブロック → CRITICAL:esc_js() または wp_localize_script() を使用してください(CWE-79)
  • wp_kses() または wp_kses_post() を使わないリッチ HTML の出力 → WARNING:安全でないタグを許す可能性があります

安全なパターン:入力時や保存時ではなく、出力時に必ずエスケープしてください。

ファイルアップロードハンドラー

以下をスキャンします:

  • wp_handle_upload() ではなく move_uploaded_file( を使用 → CRITICAL:WP のセキュリティチェックを回避します(CWE-434)
  • current_user_can() チェックの欠落 → CRITICAL:不正なファイルアップロード(CWE-862)
  • MIME タイプの検証がない → WARNING:実行可能ファイルのアップロードを許す可能性があります
  • ファイルサイズの上限がない → INFO:DoS を引き起こす可能性があります
  • wp_check_filetype_and_ext() の欠落 → WARNING:ファイルタイプの偽装が可能です(CWE-434)
JavaScript ファイル(*.js、*.jsx)

以下をスキャンします:

  • X-WP-Nonce ヘッダーを付けずに REST API を呼び出す fetch() または $.ajax() → WARNING:CSRF の脆弱性(CWE-352)
  • data に nonce を含めない $.post( ajaxurl, ...) → WARNING:CSRF の脆弱性(CWE-352)
  • innerHTML = userInput または dangerouslySetInnerHTML → CRITICAL:DOM ベースの XSS(CWE-79)
  • eval( → CRITICAL:コードインジェクションの経路(CWE-95)

すばやく検出するための検索パターン(SEC-20)

# CRITICAL: SQL injection patterns
grep -rn "\$wpdb->query(" . | grep -v "prepare"
grep -rn "\$wpdb->get_results(" . | grep -v "prepare"
grep -rn "\$wpdb->get_var(" . | grep -v "prepare"
grep -rn "\$wpdb->get_row(" . | grep -v "prepare"

# CRITICAL: XSS (unescaped output)
grep -rn "echo \$_" .
grep -rn "print \$_" .
grep -rn "<?php echo \$" . | grep -v "esc_"

# CRITICAL: Dangerous functions
grep -rn "eval(" .
grep -rn "exec(" .
grep -rn "shell_exec(" .
grep -rn "system(" .
grep -rn "passthru(" .
grep -rn "base64_decode(\$_" .
grep -rn "unserialize(\$_" .

# CRITICAL: File upload without WP functions
grep -rn "move_uploaded_file(" .

# CRITICAL: REST API without permission_callback
grep -rn "register_rest_route" . | grep -v "permission_callback"

# CRITICAL: Path traversal / dynamic includes
grep -rn "include \$_" .
grep -rn "require \$_" .
grep -rn "include_once \$_" .
grep -rn "require_once \$_" .

# WARNING: Unsanitized input usage
grep -rn "\$_GET\[" . | grep -v "sanitize_"
grep -rn "\$_POST\[" . | grep -v "sanitize_"
grep -rn "\$_REQUEST\[" . | grep -v "sanitize_"

# WARNING: Missing nonces on public AJAX
grep -rn "wp_ajax_nopriv_" .

# WARNING: Missing capability checks
grep -rn "admin_post_" . | grep -v "current_user_can"
grep -rn "wp_ajax_" . | grep -v "current_user_can"

# INFO: Missing ABSPATH check
grep -rn "<?php" . | head -20 | grep -v "ABSPATH"

# INFO: Security constants missing (check wp-config.php)
grep -n "DISALLOW_FILE_EDIT" wp-config.php
grep -n "FORCE_SSL_ADMIN" wp-config.php
grep -n "DISALLOW_UNFILTERED_HTML" wp-config.php

プラットフォームのコンテキスト

ホスティング環境によって、提供されるセキュリティレイヤーは異なります。

マネージド WordPress ホスト(WP Engine、Pantheon、Pressable、WordPress VIP など):

  • プラットフォームレベルの WAF やマルウェアスキャンを備えていることがよくあります
  • プラットフォームレベルで危険な関数(eval、exec)がブロックされる場合があります
  • 追加のセキュリティ要件について、ホストのドキュメントを確認してください
  • 移植性を保つため、コード自体も引き続き安全に記述してください

WordPress VIP:

  • 厳格なコードレビュー要件があります
  • 多くの危険な関数が禁止されています
  • 追加のセキュリティヘルパー(wpcom_vip_* 関数)を利用できます
  • 必須のパターンについては VIP のドキュメントを参照してください

セルフホスト/標準的なホスティング:

  • コードで実装したもの以外に、プラットフォームレベルのセキュリティはありません
  • 安全なコーディングの実践に対する責任がより大きくなります
  • 3 つの柱(サニタイズ、認可、エスケープ)がすべて実装されていることを確認してください

クイックリファレンス:重要なセキュリティパターン

XSS の防止(出力のエスケープ)
// ❌ CRITICAL: Unescaped output (CWE-79)
<h1><?php echo $_GET['title']; ?></h1>
<input value="<?php echo $user_input; ?>">
<a href="<?php echo $url; ?>">Link</a>
<script>var data = "<?php echo $json; ?>";</script>

// ✅ GOOD: Context-appropriate escaping
<h1><?php echo esc_html( $_GET['title'] ); ?></h1>
<input value="<?php echo esc_attr( $user_input ); ?>">
<a href="<?php echo esc_url( $url ); ?>">Link</a>
<script>var data = <?php echo wp_json_encode( $data ); ?>;</script>

// ❌ CRITICAL: Early escaping (wrong pattern)
$title = esc_html( $_POST['title'] );
update_post_meta( $post_id, 'title', $title ); // Stores HTML entities

// ✅ GOOD: Late escaping (correct pattern)
$title = sanitize_text_field( $_POST['title'] );
update_post_meta( $post_id, 'title', $title );
echo '<h1>' . esc_html( get_post_meta( $post_id, 'title', true ) ) . '</h1>';

// ✅ GOOD: Rich HTML with wp_kses_post() for trusted content
echo wp_kses_post( $content ); // Allows safe HTML tags only

// ✅ GOOD: Custom allowed tags with wp_kses()
$allowed_tags = array(
    'a' => array( 'href' => array(), 'title' => array() ),
    'br' => array(),
    'strong' => array(),
);
echo wp_kses( $user_content, $allowed_tags );
SQL インジェクションの防止
// ❌ CRITICAL: Direct interpolation (CWE-89)
$results = $wpdb->get_results( "SELECT * FROM {$wpdb->users} WHERE ID = $user_id" );
$results = $wpdb->query( "UPDATE {$wpdb->posts} SET post_title = '$title'" );

// ✅ GOOD: Use $wpdb->prepare() with placeholders
$results = $wpdb->get_results( $wpdb->prepare(
    "SELECT * FROM {$wpdb->users} WHERE ID = %d",
    $user_id
) );

$wpdb->query( $wpdb->prepare(
    "UPDATE {$wpdb->posts} SET post_title = %s WHERE ID = %d",
    $title,
    $post_id
) );

// ✅ GOOD: Multiple placeholders
$wpdb->get_results( $wpdb->prepare(
    "SELECT * FROM {$wpdb->posts} WHERE post_status = %s AND post_author = %d",
    $status,
    $author_id
) );

// ❌ WARNING: LIKE without esc_like()
$wpdb->prepare( "SELECT * FROM {$wpdb->posts} WHERE post_title LIKE '%%{$term}%%'" );

// ✅ GOOD: Use esc_like() for LIKE queries
$like_term = '%' . $wpdb->esc_like( $term ) . '%';
$wpdb->get_results( $wpdb->prepare(
    "SELECT * FROM {$wpdb->posts} WHERE post_title LIKE %s",
    $like_term
) );

// ✅ OK: Hardcoded SQL without user input (admin context)
if ( current_user_can( 'manage_options' ) ) {
    $wpdb->query( "DELETE FROM {$wpdb->options} WHERE option_name = 'temp_setting'" );
}
CSRF/Nonce の検証(3 ステップのパターン)
// ❌ CRITICAL: Missing nonce verification (CWE-352)
add_action( 'admin_post_save_settings', function() {
    update_option( 'my_setting', $_POST['value'] );
} );

// ❌ CRITICAL: Missing capability check (CWE-862)
add_action( 'admin_post_save_settings', function() {
    check_admin_referer( 'save_settings_action', 'settings_nonce' );
    update_option( 'my_setting', $_POST['value'] );
} );

// ✅ GOOD: Complete three-step pattern
add_action( 'admin_post_save_settings', function() {
    // Step 1: Verify nonce
    if ( ! isset( $_POST['settings_nonce'] ) ||
         ! wp_verify_nonce( $_POST['settings_nonce'], 'save_settings_action' ) ) {
        wp_die( 'Invalid nonce' );
    }

    // Step 2: Check capability
    if ( ! current_user_can( 'manage_options' ) ) {
        wp_die( 'Insufficient permissions' );
    }

    // Step 3: Sanitize input
    $value = sanitize_text_field( $_POST['value'] );
    update_option( 'my_setting', $value );

    wp_redirect( admin_url( 'admin.php?page=settings&updated=true' ) );
    exit;
} );

// ✅ GOOD: Form with nonce field
<form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>">
    <?php wp_nonce_field( 'save_settings_action', 'settings_nonce' ); ?>
    <input type="hidden" name="action" value="save_settings">
    <input type="text" name="value" value="<?php echo esc_attr( $current_value ); ?>">
    <?php submit_button(); ?>
</form>

// ✅ GOOD: AJAX with nonce
add_action( 'wp_ajax_update_user_meta', function() {
    check_ajax_referer( 'update_meta_nonce', 'nonce' );

    if ( ! current_user_can( 'edit_users' ) ) {
        wp_send_json_error( 'Insufficient permissions' );
    }

    $user_id = absint( $_POST['user_id'] );
    $value   = sanitize_text_field( $_POST['value'] );

    update_user_meta( $user_id, 'custom_field', $value );
    wp_send_json_success();
} );

// JavaScript for AJAX nonce
jQuery.post( ajaxurl, {
    action: 'update_user_meta',
    nonce: myAjax.nonce, // From wp_localize_script()
    user_id: 123,
    value: 'new value'
} );
認可/権限チェック
// ❌ CRITICAL: No capability check (CWE-862)
add_action( 'admin_post_delete_user', function() {
    wp_delete_user( $_POST['user_id'] );
} );

// ❌ WARNING: Incorrect capability (CWE-863)
add_action( 'admin_post_delete_user', function() {
    if ( ! current_user_can( 'edit_posts' ) ) { // Wrong capability!
        wp_die( 'Insufficient permissions' );
    }
    wp_delete_user( $_POST['user_id'] );
} );

// ✅ GOOD: Correct capability check
add_action( 'admin_post_delete_user', function() {
    if ( ! current_user_can( 'delete_users' ) ) {
        wp_die( 'Insufficient permissions' );
    }

    check_admin_referer( 'delete_user_action', 'delete_nonce' );
    wp_delete_user( absint( $_POST['user_id'] ) );
} );

// ✅ GOOD: Check user can edit specific post
if ( ! current_user_can( 'edit_post', $post_id ) ) {
    wp_die( 'You cannot edit this post' );
}

// ✅ GOOD: REST API permission callback
register_rest_route( 'myapp/v1', '/users/(?P<id>\d+)', array(
    'methods'             => 'DELETE',
    'callback'            => 'myapp_delete_user',
    'permission_callback' => function( $request ) {
        return current_user_can( 'delete_users' );
    },
) );

// ✅ OK: __return_true for public read endpoint
register_rest_route( 'myapp/v1', '/posts', array(
    'methods'             => 'GET',
    'callback'            => 'myapp_get_posts',
    'permission_callback' => '__return_true', // Public read is OK
) );
ファイルアップロードのセキュリティ
// ❌ CRITICAL: Direct move_uploaded_file() (CWE-434)
if ( isset( $_FILES['upload'] ) ) {
    move_uploaded_file( $_FILES['upload']['tmp_name'], '/uploads/' . $_FILES['upload']['name'] );
}

// ❌ CRITICAL: No capability check (CWE-862)
$file = wp_handle_upload( $_FILES['upload'], array( 'test_form' => false ) );

// ✅ GOOD: Complete file upload security
add_action( 'admin_post_upload_file', function() {
    // Step 1: Verify nonce
    check_admin_referer( 'upload_file_action', 'upload_nonce' );

    // Step 2: Check capability
    if ( ! current_user_can( 'upload_files' ) ) {
        wp_die( 'Insufficient permissions' );
    }

    // Step 3: Validate file exists
    if ( ! isset( $_FILES['upload'] ) || UPLOAD_ERR_OK !== $_FILES['upload']['error'] ) {
        wp_die( 'File upload failed' );
    }

    // Step 4: Use wp_handle_upload() (validates MIME type, extension)
    require_once( ABSPATH . 'wp-admin/includes/file.php' );
    $file = wp_handle_upload( $_FILES['upload'], array( 'test_form' => false ) );

    if ( isset( $file['error'] ) ) {
        wp_die( 'Upload error: ' . esc_html( $file['error'] ) );
    }

    // Step 5: Store file info
    $attachment_id = wp_insert_attachment( array(
        'post_title'     => sanitize_file_name( $_FILES['upload']['name'] ),
        'post_mime_type' => $file['type'],
        'post_status'    => 'inherit',
    ), $file['file'] );
} );

// ✅ GOOD: Restrict allowed MIME types
add_filter( 'upload_mimes', function( $mimes ) {
    // Remove potentially dangerous types
    unset( $mimes['exe'] );
    unset( $mimes['php'] );

    // Add custom allowed types if needed
    $mimes['svg'] = 'image/svg+xml';
    return $mimes;
} );

// ✅ GOOD: File type validation
$filetype = wp_check_filetype_and_ext( $_FILES['upload']['tmp_name'], $_FILES['upload']['name'] );
if ( ! $filetype['ext'] ) {
    wp_die( 'Invalid file type' );
}
オブジェクトインジェクションの防止
// ❌ CRITICAL: Unserialize user input (CWE-502)
$data = unserialize( $_POST['data'] );

// ❌ CRITICAL: Unserialize from cookie
$data = unserialize( $_COOKIE['cart_data'] );

// ✅ GOOD: Use JSON instead of serialize for user-facing data
$data = json_decode( $_POST['data'], true );
if ( JSON_ERROR_NONE !== json_last_error() ) {
    wp_die( 'Invalid JSON' );
}

// ✅ OK: Unserialize trusted internal data only
$data = get_option( 'my_internal_setting' ); // Already unserialized by WP
$meta = get_post_meta( $post_id, 'my_field', true ); // Already unserialized by WP
危険な関数
// ❌ CRITICAL: eval() with user input (CWE-95)
eval( $_POST['code'] );

// ❌ CRITICAL: exec() with user input (CWE-78)
exec( 'ls -la ' . $_GET['dir'] );

// ❌ CRITICAL: shell_exec() with user input
$output = shell_exec( 'grep ' . $_POST['search'] . ' file.txt' );

// ✅ GOOD: Avoid eval(), exec(), shell_exec() entirely in web context
// If absolutely necessary, whitelist input strictly
$allowed_dirs = array( 'uploads', 'cache' );
$dir = sanitize_key( $_GET['dir'] );
if ( in_array( $dir, $allowed_dirs, true ) ) {
    exec( 'ls -la ' . escapeshellarg( $dir ), $output );
}
直接ファイルアクセスの防止
// ❌ WARNING: Missing ABSPATH check
<?php
// Plugin code here without security check

// ✅ GOOD: ABSPATH check at top of every PHP file
<?php
defined( 'ABSPATH' ) || exit;

// Plugin code here

// ✅ GOOD: Alternative pattern
if ( ! defined( 'ABSPATH' ) ) {
    die( 'Direct access not permitted.' );
}
入力のサニタイズ
// ❌ WARNING: No sanitization (CWE-20)
$title = $_POST['title'];
update_post_meta( $post_id, 'title', $title );

// ✅ GOOD: Sanitize based on expected data type
$title       = sanitize_text_field( $_POST['title'] );
$email       = sanitize_email( $_POST['email'] );
$url         = sanitize_url( $_POST['url'] );
$filename    = sanitize_file_name( $_POST['filename'] );
$key         = sanitize_key( $_POST['key'] );
$int         = absint( $_POST['count'] );
$float       = floatval( $_POST['price'] );
$textarea    = sanitize_textarea_field( $_POST['description'] );

// ✅ GOOD: Array of integers
$ids = array_map( 'absint', (array) $_POST['ids'] );

// ✅ GOOD: Validate against whitelist
$allowed_types = array( 'post', 'page', 'product' );
$type = sanitize_key( $_POST['type'] );
if ( ! in_array( $type, $allowed_types, true ) ) {
    wp_die( 'Invalid type' );
}

重大度の定義(SEC-22)

重大度説明
CRITICAL認証なしで悪用可能、またはデータ侵害、RCE、権限昇格につながるもの。例:公開エンドポイントでの SQL インジェクション、エスケープされていない出力での XSS、管理者操作での nonce の欠落、権限チェックのないファイルアップロード、ユーザー入力を使った eval()
WARNING認証があれば悪用可能、または特定の条件が必要なもの。例:管理者専用コードでの XSS、ログイン済みユーザー向け操作での権限チェックの欠落、管理者コンテキストでの SQL インジェクション、不完全な入力サニタイズ
INFO多層防御の強化や堅牢化の機会。例:ABSPATH チェックの欠落、wp-config.php でのセキュリティ定数の未設定、セキュリティヘッダーの欠落

コンテキストによる調整:

  • 管理者専用コード:重大度を1段階下げます(CRITICAL → WARNING)
  • WP-CLI/cron コンテキスト:nonce は不要です(CSRF リスクがないため)
  • REST API:認証モデルが異なります(nonce ではなく permission_callback を使用)
  • 公開コード:最も重大度が高くなります(認証の障壁がないため)

出力形式(SEC-23)

ファイルと重大度レベルごとに検出事項をまとめます。

## Security Review: [filename]

### CRITICAL Issues
**Line X** | CWE-79 | XSS via unescaped output
- **Issue**: User input echoed directly without escaping
- **Risk**: Allows attackers to inject malicious JavaScript
- **Context**: Public-facing template (HIGH severity)

❌ **BAD:**
```php
<h1><?php echo $_GET['title']; ?></h1>

✅ 良好:

<h1><?php echo esc_html( $_GET['title'] ); ?></h1>
WARNING の問題

行 Y | CWE-352 | nonce 検証の欠落

  • 問題:フォームハンドラーに wp_verify_nonce() がありません
  • リスク:CSRF 攻撃によって状態が変更される可能性があります
  • コンテキスト:管理者専用(重大度を引き下げ)

[不適切/良好なコードのペア]

INFO の推奨事項

行 Z | 多層防御 | ABSPATH チェックの欠落

  • ファイルの先頭に defined( 'ABSPATH' ) || exit; を追加します
まとめ
  • 合計:CRITICAL 1件、WARNING 1件、INFO 1件
  • リスクレベル:HIGH(悪用可能な CRITICAL の問題が存在)
  • 直ちに行う対応:行 X を修正します(エスケープされていない出力)

## Common Mistakes (SEC-24)

When performing security reviews, avoid these false positives:

| Mistake | Why It's Wrong | Context |
|---------|----------------|---------|
| Flagging missing nonces in WP-CLI commands | WP-CLI runs server-side, no CSRF risk | Nonces only needed for HTTP requests |
| Flagging `wp_kses_post()` as insufficient escaping | `wp_kses_post()` is valid for trusted content with safe HTML | Allows `<p>`, `<a>`, `<strong>`, etc. - appropriate for post content |
| Flagging admin-only `$wpdb->query()` with hardcoded SQL | No user input = no injection risk | Review for user input presence, not just prepare() usage |
| Flagging `current_user_can()` inside REST `permission_callback` as redundant | This is the CORRECT location for capability checks in REST API | REST API auth model, not redundant |
| Flagging `esc_html( get_the_title() )` as double-escaping | WP core sanitizes on storage, but late escaping is still best practice | Not technically double-escaping, WP stores raw data |
| Flagging `update_option()` in admin context as vulnerability | Options API is safe for admin use | Check capability first, then update_option() is fine |
| Flagging REST API with `__return_true` permission on GET endpoints | Public read access is often intentional | Only flag on write operations (POST, PUT, DELETE) |
| Flagging `sanitize_callback` in `register_setting()` as missing sanitization | Settings API handles sanitization via callback | This is the correct pattern for Settings API |

## Security Constants Check (SEC-19)

Review `wp-config.php` for security hardening constants:

```php
// ✅ GOOD: Disable file editor (INFO-level recommendation)
define( 'DISALLOW_FILE_EDIT', true );

// ✅ GOOD: Force SSL for admin (INFO if SSL available)
define( 'FORCE_SSL_ADMIN', true );

// ✅ GOOD: Disable unfiltered HTML for all users including admins
define( 'DISALLOW_UNFILTERED_HTML', true );

// ✅ GOOD: Disable plugin/theme installation (high-security environments)
define( 'DISALLOW_FILE_MODS', true );

// ✅ GOOD: Custom authentication keys (CRITICAL if using defaults)
define( 'AUTH_KEY',         'put-unique-phrase-here' ); // Use https://api.wordpress.org/secret-key/1.1/salt/
define( 'SECURE_AUTH_KEY',  'put-unique-phrase-here' );
define( 'LOGGED_IN_KEY',    'put-unique-phrase-here' );
define( 'NONCE_KEY',        'put-unique-phrase-here' );
define( 'AUTH_SALT',        'put-unique-phrase-here' );
define( 'SECURE_AUTH_SALT', 'put-unique-phrase-here' );
define( 'LOGGED_IN_SALT',   'put-unique-phrase-here' );
define( 'NONCE_SALT',       'put-unique-phrase-here' );

詳細な参考資料

脆弱性のパターンと例を網羅的に確認するには、以下の参考資料を読み込んでください。

タスク読み込む参考資料
CWE マッピングを含む包括的な脆弱性カタログreferences/vulnerability-patterns.md
出力のエスケープパターンとコンテキスト別の関数references/escaping-guide.md
データ型ごとの入力サニタイズパターンreferences/sanitization-guide.md
認可と権限チェックのパターンreferences/auth-patterns.md
nonce の生成、検証、CSRF 防止references/nonce-csrf-guide.md

注:標準的なセキュリティレビューでは、この SKILL.md に必要なパターンがすべて記載されています。包括的な CWE カタログ、エッジケース、またはプラットフォーム固有のセキュリティ要件(WordPress VIP など)が必要な場合は、参考資料を読み込んでください。

コメント