2026年5月20日
2026年5月20日
WordPressのSQLインジェクション対策をする方法
はじめに
SQLインジェクションは、攻撃者がデータベースクエリに悪意のあるSQL文を挿入する攻撃です。WordPressでは$wpdb->prepare()を使ったプレースホルダーによるパラメータバインディングが基本的な対策です。適切に実装することでデータ漏洩・改ざん・削除を防げます。
症状・原因
- セキュリティスキャンでSQLインジェクション脆弱性が検出された
- プラグインやテーマで
$wpdb->query("SELECT ... WHERE id = " . $_GET['id'])のような直接埋め込みがある - ユーザー入力を直接SQLクエリに連結している
sqlmap等のツールでデータベースが抽出できた
解決手順
ステップ1:脆弱なコードパターンを検出する
# SQL クエリに変数を直接連結しているパターンを検索
grep -rn "\$wpdb->query.*\$_GET\|\$wpdb->query.*\$_POST" /var/www/html/wp-content/
grep -rn "\$wpdb->get_results.*\$_GET\|\$wpdb->get_results.*\$_POST" /var/www/html/wp-content/
# prepare を使っていないクエリを検索
grep -rn "->query(\"SELECT\|->query('SELECT\|->get_results(\"SELECT" /var/www/html/wp-content/themes/
grep -rn "->query(\"SELECT\|->query('SELECT\|->get_results(\"SELECT" /var/www/html/wp-content/plugins/
# 文字列連結でSQLを組み立てているパターン
grep -rn '"\s*\.\s*\$' /var/www/html/wp-content/ | grep -i "select\|insert\|update\|delete\|where"
ステップ2:$wpdb->prepare() でプレースホルダーを使う
// functions.php / プラグイン: 安全なDBクエリの書き方
global $wpdb;
// ① 整数値のプレースホルダー(%d)
// 悪い例
$user_id = $_GET['user_id'];
$results = $wpdb->get_results("SELECT * FROM {$wpdb->users} WHERE ID = $user_id");
// 良い例
$user_id = absint($_GET['user_id']); // 整数に変換
$results = $wpdb->get_results(
$wpdb->prepare("SELECT * FROM {$wpdb->users} WHERE ID = %d", $user_id)
);
// ② 文字列値のプレースホルダー(%s)
// 悪い例
$name = $_POST['name'];
$wpdb->query("UPDATE {$wpdb->users} SET display_name = '$name' WHERE ID = 1");
// 良い例
$name = sanitize_text_field($_POST['name']);
$wpdb->update(
$wpdb->users,
['display_name' => $name], // データ(自動エスケープ)
['ID' => get_current_user_id()], // 条件(自動エスケープ)
['%s'], // データのフォーマット
['%d'] // 条件のフォーマット
);
// ③ 浮動小数点のプレースホルダー(%f)
$price = (float) $_POST['price'];
$wpdb->get_results(
$wpdb->prepare("SELECT * FROM {$wpdb->prefix}products WHERE price <= %f", $price)
);
// ④ 複数の値を IN 句に渡す場合
$ids = array_map('absint', $_POST['ids']); // 全て整数に変換
$placeholders = implode(',', array_fill(0, count($ids), '%d'));
$results = $wpdb->get_results(
$wpdb->prepare(
"SELECT * FROM {$wpdb->posts} WHERE ID IN ($placeholders)",
...$ids
)
);
ステップ3:$wpdb の安全なメソッドを使う
// functions.php: $wpdb の組み込みメソッドで安全にCRUD操作
global $wpdb;
$table = $wpdb->prefix . 'my_table';
// ① INSERT: $wpdb->insert(自動エスケープ)
$wpdb->insert(
$table,
[
'user_id' => get_current_user_id(),
'content' => sanitize_textarea_field($_POST['content']),
'created_at' => current_time('mysql'),
],
['%d', '%s', '%s']
);
// ② UPDATE: $wpdb->update(自動エスケープ)
$wpdb->update(
$table,
['status' => 'published'], // 更新データ
['id' => absint($_POST['id'])], // WHERE条件
['%s'], // データのフォーマット
['%d'] // 条件のフォーマット
);
// ③ DELETE: $wpdb->delete(自動エスケープ)
$wpdb->delete(
$table,
['id' => absint($_GET['id'])],
['%d']
);
// ④ get_var / get_row / get_col / get_results + prepare
$count = $wpdb->get_var(
$wpdb->prepare("SELECT COUNT(*) FROM $table WHERE user_id = %d", get_current_user_id())
);
ステップ4:入力値を事前にサニタイズ・バリデーションする
// functions.php: prepare の前に入力値を検証・サニタイズ
// ① 整数値の検証
function validate_id(mixed $value): int {
$id = absint($value);
if ($id <= 0) {
wp_die('無効なIDです。', 400);
}
return $id;
}
// ② 文字列の検証(長さ・許可文字)
function validate_search_term(string $term): string {
$term = sanitize_text_field($term);
if (strlen($term) > 100) {
wp_die('検索キーワードが長すぎます。', 400);
}
return $term;
}
// ③ REST API エンドポイントでの検証
add_action('rest_api_init', function(): void {
register_rest_route('myapi/v1', '/items/(?P<id>\d+)', [
'methods' => 'GET',
'callback' => function(\WP_REST_Request $request): mixed {
global $wpdb;
$id = absint($request->get_param('id'));
return $wpdb->get_row(
$wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}items WHERE id = %d",
$id
)
);
},
'permission_callback' => '__return_true',
'args' => [
'id' => [
'validate_callback' => fn($v) => is_numeric($v) && $v > 0,
'sanitize_callback' => 'absint',
],
],
]);
});
ステップ5:WAF と エラー表示の無効化で防御を強化する
# ① WordPress デバッグ表示を無効化(エラーからDB情報が漏れるのを防ぐ)
# wp-config.php
# define('WP_DEBUG', false);
# define('WP_DEBUG_DISPLAY', false);
# define('DISPLAY_ERRORS', 0);
wp config set WP_DEBUG false --raw
wp config set WP_DEBUG_DISPLAY false --raw
# ② Nginx の WAF モジュールで SQL インジェクションを検出
# ModSecurity または NAXSI でリクエストをフィルタリング
grep -r "UNION\|SELECT.*FROM\|DROP TABLE\|INSERT INTO" /var/log/nginx/access.log | tail -20
# ③ Fail2ban で SQLi を検出してIPをBAN
cat > /etc/fail2ban/filter.d/wordpress-sqli.conf << 'EOF'
[Definition]
failregex = ^<HOST>.*"(GET|POST).*(UNION|SELECT.*FROM|DROP|INSERT.*INTO|1=1|OR 1=1)
ignoreregex =
EOF
注意事項
$wpdb->prepare()の第1引数にはプレースホルダー(%d、%s、%f)を使用し、値は第2引数以降に渡してください。%sは文字列をクォートで囲んで自動エスケープしますLIKE句を使用する場合は$wpdb->esc_like()を使って%と_をエスケープした上で%sプレースホルダーを組み合わせてください:$wpdb->prepare("... LIKE %s", '%' . $wpdb->esc_like($term) . '%')- テーブル名やカラム名にはプレースホルダーが使えません。これらはユーザー入力から生成する場合は許可リスト(allowlist)でバリデーションしてください
まとめ
SQLインジェクション対策は①grep -rn "->query.*\$_GET"で脆弱パターンを検出、②$wpdb->prepare("... WHERE id = %d", $id)でプレースホルダーを使用、③$wpdb->insert()・update()・delete()で自動エスケープ、④absint()・sanitize_text_field()でDB処理前にサニタイズ、⑤WP_DEBUG=falseでエラー表示を非公開にします。