$wpdb->prepare で LIKE と IN 句を安全に書く方法

結論

  • LIKE 検索の % は、SQL文に直接書かず、値の側に含めて %s で渡します。ユーザー入力の % や _ は $wpdb->esc_like() で先にエスケープします。
  • IN 句は、要素数ぶんの %d(文字列なら %s)を implode() で組み立て、値は配列で prepare() に渡します。
  • テーブル名やカラム名は、WordPress 6.2.0 から追加された %i で渡せます。それ以前の環境では使えません。
  • sanitize_text_field() は SQLインジェクション対策ではありません。SQLに値を入れるときは必ずプレースホルダを使います($wpdb->prepare() の使い方|説明・引数・注意点)。

原因・仕組み

$wpdb->prepare() は、%s(文字列)、%d(整数)、%f(小数)、%i(識別子)の4つのプレースホルダに対応しています。値はエスケープされ、文字列は引用符で囲まれます。

問題は、LIKE の % とプレースホルダの % が見た目で同じことです。公式ドキュメントも、LIKE のワイルドカードは「完全なLIKE文字列を持つ置換引数」で渡すよう求めています。そのため、次のように書きます。

やりたいこと 書き方
前後一致(部分一致) '%' . $wpdb->esc_like( $kw ) . '%' を %s に渡す
前方一致 $wpdb->esc_like( $kw ) . '%'
数値のIN IN (%d,%d,%d) を配列から生成
文字列のIN IN (%s,%s) を配列から生成

手順(サンプルコード)

global $wpdb;

// 1) LIKE: ユーザー入力の % と _ を esc_like で無効化してから % を足す
$keyword = isset( $_GET['q'] ) ? sanitize_text_field( wp_unslash( $_GET['q'] ) ) : '';
$like    = '%' . $wpdb->esc_like( $keyword ) . '%';
$ids = $wpdb->get_col(
    $wpdb->prepare( "SELECT ID FROM {$wpdb->posts} WHERE post_status = %s AND post_title LIKE %s", 'publish', $like )
);

// 2) IN: 個数が可変のとき
$post_ids     = array_map( 'absint', (array) $post_ids );
$placeholders = implode( ',', array_fill( 0, count( $post_ids ), '%d' ) );
$sql          = $wpdb->prepare( "SELECT ID, post_title FROM {$wpdb->posts} WHERE ID IN ($placeholders)", $post_ids );
$rows         = $wpdb->get_results( $sql );

2) では、$post_ids が空配列だと IN () になり、SQLエラーになります。空のときは先に return します。

動作確認(検証環境と結果)

WordPress 7.1.2(ja)、PHP 8.2.12、MariaDB 10.4.32。php run.php wpdb-prepare-like-in.php で確認しました。

SELECT ID FROM wp_posts WHERE ID IN (3,5,7)
SELECT ID FROM wp_posts WHERE post_type IN ('post','pa'ge')
SELECT * FROM `wp_posts` WHERE ID = 1          // %i と %d
count Hello: 1                                  // 実際にクエリを実行した件数
  • 配列 array(3, 5, '7; DROP TABLE x') を %d に渡すと、3つ目は 7 に変換されました(文字列の後半は捨てられる)。
  • 文字列の pa'ge は pa'ge とエスケープされました。
  • % を含む LIKE の prepare() 結果を echo すると、% の部分が長い16進の文字列(プレースホルダ用のエスケープ値)に置き換わって表示されました。これは prepare() が内部で % を保護している表示で、クエリの実行時(get_var() など)には元に戻ります。デバッグ時に echo した値を直接DBに貼らないようにしてください。
  • 悪い例として、LIKE '%foo%' AND ID = %d と直接書く方法を試したところ、戻り値は空文字でした('%foo%' の %f がプレースホルダと解釈され、引数の個数が合わずエラー扱いになった)。このように書くのは避けます。

$wpdb->prepare で LIKE と IN 句を安全に書く方法の注意点

  • esc_like() は LIKE 用の % _ のエスケープだけを行います。SQLインジェクション対策は prepare() が担当し、両方必要です。順序は esc_like() → prepare() です。
  • prepare() に値を渡さずに % が無い文字列を渡すと、公式では警告が出ると書かれています。値がないSQLに prepare() は不要です。
  • 番号付きプレースホルダ(%1$s)は、公式ドキュメントにあるとおり引用符が自動で付きません(検証環境でも SELECT %1$s に a'b を渡すと a'b と引用符なしで展開され、引数の個数不一致の Notice も出ました)。基本は %s %d を使います。引数は個別か配列のどちらかで統一します(混ぜない)。
  • %d に渡す値は、事前に absint() などで整数にしておくと意図が明確です。

$wpdb->prepare で LIKE と IN 句を安全に書く方法でよくあるミス

  • "... LIKE '%{$kw}%'" と変数を直接埋め込む(悪い例。インジェクションの原因)。
  • prepare() の結果をさらに文字列連結して使う。
  • IN 句で %s に数値の配列を渡し、数値が '1' のように引用される。
  • テーブル名を %s で渡し、'wp_posts' と引用符付きになる。

$wpdb->prepare で LIKE と IN 句を安全に書く方法のチェックリスト

  • 値はすべてプレースホルダで渡しているか。
  • LIKE の入力値に esc_like() を使っているか。
  • IN 句の配列が空のケースを先に処理しているか。
  • 識別子は、固定値か、%i(6.2以降)で渡しているか。

$wpdb->prepare で LIKE と IN 句を安全に書く方法のFAQ(よくある質問)

Q. $wpdb->esc_sql() で足りませんか?
A. 古い書き方ですが、新しいコードでは prepare() を使います(esc_sql() の使い方|説明・引数・注意点)。

Q. LIKE で大文字小文字を区別しますか?
A. DBの照合順序(collation)によります。WordPress標準では、通常は区別しません。

Q. 検索なら WP_Query の s で十分では?
A. 投稿の検索なら WP_Query を優先してください。独自テーブルや特殊な条件のときにだけ $wpdb を使います。

筆者の見解($wpdb->prepare で LIKE と IN 句を安全に書く方法)

SQLは「値はすべて prepare() に任せ、文字列連結は構造(テーブル名と個数ぶんのプレースホルダ)だけ」と決めておくと、レビューが楽になると考えます。LIKE と IN は、どちらも機械的に書けるのに事故が多い箇所なので、小さなヘルパー関数にまとめて使い回すのがよいと思います。

$wpdb->prepare で LIKE と IN 句を安全に書く方法の関連項目

出典(一次情報)

タイトルとURLをコピーしました