WordPress でテーマやプラグインを書くとき、避けて通れないのが「エスケープ」と「サニタイズ」です。難しそうに聞こえますが、要は「画面に出すときは出力用に整える」「保存するときは入力を掃除する」という2つの作業のことです。この記事では、なぜこれらが必要なのか(XSS 対策)から始めて、esc_html() や esc_attr() などの出力エスケープ関数、sanitize_text_field() などの入力サニタイズ関数の使い分けを、実際のテンプレートやメタ保存のコード例とあわせて初心者向けに解説します。
目次
なぜエスケープとサニタイズが必要なのか
ユーザーが入力した値や、データベースから取り出した値をそのまま HTML に出力すると、悪意のあるコードが混ざっていた場合にそのまま実行されてしまうことがあります。たとえば投稿のタイトルに <script> タグが仕込まれていて、それを無防備に出力すると、閲覧者のブラウザでスクリプトが動いてしまいます。これがいわゆる XSS(クロスサイトスクリプティング)です。
こうした事故を防ぐために、WordPress では2つのタイミングで値を安全に扱います。1つは画面に出力する直前の「エスケープ」、もう1つはデータを保存する入力時の「サニタイズ」です。両者は名前が似ていますが役割が違うので、まずはこの違いをはっきりさせておきましょう。
エスケープは「出力直前」、サニタイズは「保存時(入力時)」
原則はとてもシンプルです。エスケープは値を画面に出力する直前に行います。 < や >、" といった記号を < のような実体参照に変換し、HTML やスクリプトとして解釈されないようにします。「どこに出すか(テキストか属性か URL か)」によって使う関数が変わるのがポイントです。
一方、サニタイズはフォームや $_POST から受け取った値を保存する直前に行います。 余計なタグや改行、不正な文字を取り除いて、データを「きれいな状態」にしてから保存します。エスケープが出力側の防御なのに対し、サニタイズは入力側の掃除だと考えると分かりやすいでしょう。
重要なのは、サニタイズして保存した値であっても、出力するときには改めてエスケープするということです。データベースの中身が安全だとは限らず(別の経路で書き込まれることもある)、出力時のエスケープは「最後の砦」として常に行うのが WordPress の流儀です。
出力時に使うエスケープ関数
出力エスケープ関数は、値を「どこに出すか」によって使い分けます。テキストの本文として出すのか、タグの属性値に入れるのか、URL なのか、テキストエリアの中身なのかで、エスケープすべき記号が変わるためです。代表的な関数を整理すると次のとおりです。
| 関数 | 使う場面 |
|---|---|
esc_html( $text ) | HTML のテキスト本文として出力するとき(タグを文字として表示する) |
esc_attr( $text ) | タグの属性値(value、title、class など)の中に出力するとき |
esc_url( $url ) | リンクの href や画像の src など、URL を出力するとき |
esc_textarea( $text ) | <textarea> の中身として出力するとき |
esc_js( $text ) | インラインの JavaScript(文字列リテラルの中)に値を埋め込むとき |
多くの場面で出番が多いのは esc_html() と esc_attr() の2つです。「テキストとして見せるなら esc_html()」「属性の中に入れるなら esc_attr()」と覚えておくと、ほとんどのケースに対応できます。
テンプレートでの基本的な使い方
テーマのテンプレートで値を echo するときは、出力する関数の戻り値をエスケープ関数で包みます。次の例は、投稿タイトルをテキストとして、投稿のパーマリンクを href 属性として、それぞれ安全に出力しています。
<?php
// タイトルはテキスト本文として出力するので esc_html()
$title = get_the_title();
?>
<h1><?php echo esc_html( $title ); ?></h1>
<?php
// URL は esc_url()、リンク文言は esc_html() で分けてエスケープする
$permalink = get_permalink();
?>
<a href="<?php echo esc_url( $permalink ); ?>">
<?php echo esc_html( get_the_title() ); ?>
</a>
属性値の中に値を入れる場合は esc_attr() を使います。たとえばカスタムフィールドの値を input の value に出すときは、次のように書きます。esc_attr() はダブルクオートなどをエスケープするので、属性が途中で閉じられて別の属性を注入される事故を防げます。
<?php $subtitle = get_post_meta( get_the_ID(), 'subtitle', true ); ?>
<input
type="text"
name="subtitle"
value="<?php echo esc_attr( $subtitle ); ?>"
>
<?php // textarea の中身は esc_textarea() を使う ?>
<textarea name="note"><?php echo esc_textarea( $note ); ?></textarea>
翻訳とエスケープをまとめて行う関数
テーマやプラグインを多言語対応させるときは、翻訳関数(__() や _e())を使います。これらにエスケープを組み合わせた便利な関数も用意されています。esc_html__() は「翻訳した文字列を esc_html() して返す」関数で、esc_html_e() は「翻訳して esc_html() してそのまま echo する」関数です。属性向けには esc_attr__() / esc_attr_e() があります。
<?php
// 翻訳した文字列をエスケープして echo する
esc_html_e( 'もっと読む', 'my-theme' );
?>
<?php
// 戻り値として受け取りたいときは esc_html__()
$label = esc_html__( '送信する', 'my-theme' );
echo $label;
?>
翻訳テキストはそのまま信頼せず、こうしたエスケープ付きの関数を使うのが安全です。第2引数の 'my-theme' はテキストドメインで、自分のテーマ・プラグインに合わせて指定します。
入力を保存する前に使うサニタイズ関数
フォームから送られてきた値(多くは $_POST に入っています)を保存する前に、サニタイズ関数で掃除します。何を保存したいかによって、適切な関数を選びます。代表的なものをまとめました。
| 関数 | 用途 |
|---|---|
sanitize_text_field( $str ) | 1行のテキスト。タグや余分な空白・改行を取り除く |
sanitize_textarea_field( $str ) | 複数行のテキスト。改行を保ちつつ掃除する |
sanitize_email( $email ) | メールアドレスとして無効な文字を取り除く |
sanitize_key( $key ) | 小文字英数字・-・_ だけを残す(キーやスラッグ向け) |
absint( $value ) | 値を「0 以上の整数」に変換する(ID や個数向け) |
sanitize_text_field() はもっとも汎用的で、1行のテキスト入力ならまずこれを使えば間違いありません。複数行のテキストエリアの値は改行を残したいので、sanitize_textarea_field() を使い分けます。数値として扱いたい ID には absint() を使うと、想定外の文字列が混ざるのを防げます。
メタ情報を保存するときの流れ
実際の保存処理では、$_POST の値をサニタイズしてから update_post_meta() に渡します。次の例は、投稿編集画面のカスタムフィールド(サブタイトルと外部リンク)を保存するコードです。save_post フックに合わせて、nonce の検証も行っています。
function my_save_post_meta( $post_id ) {
// nonce を検証して不正なリクエストをはじく
if ( ! isset( $_POST['my_meta_nonce'] )
|| ! wp_verify_nonce( $_POST['my_meta_nonce'], 'my_save_meta' ) ) {
return;
}
// 1行テキストは sanitize_text_field() で掃除して保存する
if ( isset( $_POST['subtitle'] ) ) {
$subtitle = sanitize_text_field( wp_unslash( $_POST['subtitle'] ) );
update_post_meta( $post_id, 'subtitle', $subtitle );
}
// URL は esc_url_raw() で保存用にサニタイズする
if ( isset( $_POST['external_url'] ) ) {
$url = esc_url_raw( wp_unslash( $_POST['external_url'] ) );
update_post_meta( $post_id, 'external_url', $url );
}
// 数値は absint() で 0 以上の整数にする
if ( isset( $_POST['priority'] ) ) {
$priority = absint( $_POST['priority'] );
update_post_meta( $post_id, 'priority', $priority );
}
}
add_action( 'save_post', 'my_save_post_meta' );
ここで wp_unslash() を挟んでいるのは、WordPress が $_POST の値に自動で付与するバックスラッシュ(エスケープ)を取り除くためです。サニタイズの前に wp_unslash() しておくのが定番の作法です。
なお URL の保存には、出力用の esc_url() ではなく、データベース保存用の esc_url_raw() を使う点に注意してください。esc_url() は & を & に変換するなど「表示用」の処理を含むため、保存値には不向きです。
許可したタグだけを残す wp_kses
esc_html() はすべてのタグを文字に変換してしまうため、「太字や改行など一部の HTML は許可したい」という場面には向きません。そこで使うのが wp_kses() と wp_kses_post() です。これらは「許可したタグ・属性だけを残し、それ以外を除去する」関数で、ユーザーが書いた本文に限定的に HTML を許したいときに使います。
wp_kses_post() は、投稿本文で許可されている標準的なタグ一式をそのまま許可するショートカットです。一方 wp_kses() は、許可するタグと属性を自分で配列で指定します。次の例では、a・strong・em だけを許可しています。
$allowed = array(
'a' => array(
'href' => array(),
'title' => array(),
),
'strong' => array(),
'em' => array(),
);
// 許可したタグ以外は取り除いて出力する
echo wp_kses( $user_content, $allowed );
// 投稿本文と同じ許可セットでよいなら wp_kses_post() が手軽
echo wp_kses_post( $user_content );
wp_kses 系はやや重い処理なので、HTML を一切残す必要がない場面では esc_html() を使い、HTML を限定的に許したいときだけ wp_kses 系を使う、という切り分けがおすすめです。
エスケープしてもタグがそのまま表示されてしまうとき
エスケープ周りでは「思ったとおりに表示されない」というトラブルがよく起きます。原因はだいたいパターンが決まっているので、代表的なものを見ていきましょう。
タグを表示させたいわけではないのに <br> などが文字で出る
「<br> が改行されず、そのまま文字として画面に出てしまう」という場合、HTML として効かせたい部分にまで esc_html() をかけているのが原因です。esc_html() はタグを無効化する関数なので、当然タグは文字になります。改行などの HTML を効かせたいなら、その部分はエスケープせずに出力するか、許可タグを残せる wp_kses_post() を使います。「タグを見せたいのか、効かせたいのか」を意識して関数を選ぶのが解決の鍵です。
記号が &amp; のように増える二重エスケープ
& が & になって表示される、いわゆる二重エスケープは、すでにエスケープ済みの値をもう一度エスケープすると起きます。たとえば the_title() のように内部でエスケープ済みの値を出す関数の結果を、さらに esc_html() で包むようなケースです。エスケープは「出力直前に1回だけ」が原則なので、すでにエスケープされている値には重ねてかけないようにします。生の値が欲しいときは get_the_title() のような「取得用」の関数を使い、出力時に自分で1回だけエスケープすると、二重エスケープを避けやすくなります。
属性の中で値が途切れてレイアウトが崩れる
属性値に esc_html() を使うと、ダブルクオートが十分にエスケープされず、値の中にクオートが含まれていると属性がそこで途切れてしまうことがあります。属性の中に出すときは必ず esc_attr() を使ってください。テキスト本文用の esc_html() と属性用の esc_attr() は、エスケープする対象が異なるため、置く場所に合わせて正しく選ぶ必要があります。
まとめ
エスケープとサニタイズは、WordPress を安全に使ううえで欠かせない基本動作です。最後に要点を振り返っておきましょう。
- エスケープは出力直前に行う。テキストは
esc_html()、属性はesc_attr()、URL はesc_url()、テキストエリアはesc_textarea()と、出す場所で使い分ける。 - 翻訳とエスケープをまとめたい場合は
esc_html__()/esc_html_e()などを使う。 - サニタイズは保存(入力)時に行う。1行は
sanitize_text_field()、複数行はsanitize_textarea_field()、メールはsanitize_email()、キーはsanitize_key()、整数はabsint()。 - 一部の HTML を残したいときは
wp_kses()/wp_kses_post()で許可タグだけを残す。 - 保存済みの値でも、出力時には改めてエスケープするのが原則。二重エスケープに注意し、エスケープは1回だけ。
「保存するときは掃除(サニタイズ)、出すときは整形(エスケープ)」という2つの習慣を身につければ、テーマやプラグインのセキュリティはぐっと高まります。まずは普段書いている echo を、適切なエスケープ関数で包むところから始めてみてください。