コンテンツへスキップ

$wpdb->prepare() とSQLインジェクション対策の基礎 — なぜ文字列連結でクエリを組んではいけないか

WordPressのプラグインやテーマがデータベースに直接アクセスしたいとき、コアが提供するグローバルオブジェクト $wpdb を使う。このとき絶対に避けるべきなのが、ユーザー入力をそのまま文字列連結でSQL文に埋め込むことで、代わりに用意されているのが $wpdb->prepare() という関数である。今回はこの仕組みがなぜ必要で、内部で何をしているのかを整理する。

補足: SQLインジェクションとは、Webアプリケーションが組み立てるSQL文に、想定外の文字列を紛れ込ませることでデータベースへの命令を書き換えてしまう攻撃手法の総称。入力フォームや URL パラメータなど、外部から渡ってくる値を疑うところが対策の出発点になる。

なぜ文字列連結が危険なのか

次のようなコードを考える(あくまで説明用の疑似コードで、実際にWordPress上で動かすことは推奨しない)。

// 危険な例: ユーザー入力をそのまま文字列に埋め込んでいる
$title = $_GET['title'];
$sql = "SELECT * FROM {$wpdb->posts} WHERE post_title = '$title'";
$results = $wpdb->get_results($sql);

このコードは、$title がふつうの文字列であれば問題なく動く。しかし $title に ' OR '1'='1 のような値が渡されると、組み立てられるSQL文は次のようになる。

SELECT * FROM wp_posts WHERE post_title = '' OR '1'='1'

'1'='1' は常に真になるため、WHERE 句による絞り込みが実質的に無効化され、本来は特定の投稿だけを返すはずのクエリが全投稿を返してしまう。これは単純な例だが、同じ考え方を応用すれば、想定していないデータの取得・書き換え・削除につながる。「外部から渡ってきた値を、そのままSQL文の一部として組み立てる」こと自体が根本原因であり、値の中身をどれだけ注意深くチェックしても、文字列連結という組み立て方そのものを変えない限り原理的なリスクは残る。

$wpdb->prepare() が解決していること

WordPressコアが用意している解決策が $wpdb->prepare() である。使い方は次のようになる。

// 安全な例: プレースホルダを使う
$title = $_GET['title'];
$sql = $wpdb->prepare(
    "SELECT * FROM {$wpdb->posts} WHERE post_title = %s",
    $title
);
$results = $wpdb->get_results($sql);

%s の部分が「プレースホルダ」で、第2引数以降に渡した値がここに差し込まれる。ポイントは、prepare() が単純な文字列置換をしているのではなく、値の中に含まれるクォート等の特殊文字を、SQL文の構造として解釈されないようにエスケープしてから埋め込んでいる点にある。先ほどの ' OR '1'='1 のような値を渡しても、prepare() を通した後は文字列としての ' OR '1'='1 そのものが検索条件として扱われるだけで、SQL文の構造は書き換わらない。

プレースホルダの型指定はなぜ必要か

$wpdb->prepare() のプレースホルダには主に3種類ある。

プレースホルダ 用途
%s 文字列
%d 整数
%f 浮動小数点数

%d や %f を使う理由は、エスケープだけでなく値そのものを期待する型に強制変換するためでもある。たとえば投稿IDを受け取る箇所で %d を使っておけば、123abc のような値が渡ってきても整数部分の 123 だけが使われる。逆に、数値であるべき箇所に誤って %s を使うと、クォートで囲まれた文字列としてSQL文に埋め込まれてしまい、意図しない挙動やエラーの原因になりうる。プレースホルダの型を渡す値の意味に合わせて正しく選ぶことが、prepare() を使う上でのもう一つの要点になる。

LIKE 句や IN 句など、素朴に書くと落とし穴になる箇所

検索機能などで使う LIKE 句は、prepare() の基本的な使い方だけでは不十分になりやすい箇所として知られている。LIKE は % や _ を「任意の文字列」「任意の1文字」を表すワイルドカードとして解釈するため、検索キーワードにこれらの文字が含まれていると、ユーザーが意図しない検索結果につながる可能性がある。WordPressコアはこのために $wpdb->esc_like() という専用関数を用意しており、LIKE 句用の値はこの関数でワイルドカードをエスケープしてから prepare() に渡すのが正しい手順になる。

$keyword = $wpdb->esc_like($_GET['s']);
$sql = $wpdb->prepare(
    "SELECT * FROM {$wpdb->posts} WHERE post_title LIKE %s",
    '%' . $keyword . '%'
);

同様に、複数の値を IN (...) で渡したいケースも、プレースホルダを値の個数分だけ動的に並べる必要があり、単純に1つの %s で済ませることはできない。こうした「基本のプレースホルダだけでは素直に書けない」パターンがいくつか存在することも、$wpdb を使う上で覚えておく価値がある。

自社テーマがそもそも $wpdb に触れていない理由

このブログのテーマ wpmm-blog の実装を確認すると、functions.php を含むどのテーマファイルにも $wpdb への直接アクセスは一切登場しない。記事一覧・アーカイブ・検索結果はすべて WP_Query やテンプレートタグ(have_posts() / the_post() 等)経由で取得しており、生のSQL文を自前で組み立てる場面がそもそも存在しない。

これは偶然ではなく、WordPressのテーマ・プラグイン開発において推奨されている設計方針でもある。WP_Query のようなコア提供のAPIは、内部で必要な $wpdb->prepare() 相当の処理をコア自身が行っており、呼び出す側はSQL文を意識する必要がない。「そもそも生SQLを書かずに済む場面では、コアが用意した高レベルなAPIを使う」というのは、prepare() を正しく使うことと並んで、SQLインジエクション対策のもう一つの現実的な指針といえる。

まとめ

場面 対処
通常の値をSQL文に埋め込む $wpdb->prepare() のプレースホルダ(%s / %d / %f)を使う
LIKE 句で使う値 先に $wpdb->esc_like() でワイルドカードをエスケープしてから prepare() に渡す
生SQLを書かずに済む場面 WP_Query 等のコアAPIに任せ、そもそも自前でクエリを組み立てない

SQLインジェクション対策の核心は、「入力値を信用しない」という心構えを、$wpdb->prepare() という具体的な道具に落とし込むことにある。文字列連結でクエリを組む癖がついていると気づきにくい問題なので、$wpdb を使うコードを書く・読むときは、まず「プレースホルダが使われているか」を確認する習慣をつけておくとよい。