結論
- HTMLタグの間に出す文字列は
esc_html()、タグの属性値(value="..."やclass="...")に出す文字列はesc_attr()を使います。 - 今回の環境で実行すると、普通の文字列では両者の出力は同じでした。それでも関数を分けるのは、文脈を明示するためと、フィルターフックが別だからです。
- URLは
esc_url()、インラインJavaScriptはesc_js()、一部のHTMLを許可したいときはwp_kses()系を使います。属性だからといって、href にesc_attr()を使うのは避けます。 - エスケープは「出力する直前」に行います(公式の言う「Escape Late」)。保存時のサニタイズとは役割が違います。
原因・仕組み
公式ドキュメントは、データの出力先(HTML本文・属性・URL・JavaScript・textarea)によって適切な関数が変わるとしています。変数を属性の一部として使うときは、連結した文字列全体をエスケープする方が安全だとも書かれています。
エスケープが必要な理由は単純で、ユーザー由来の文字列が " や < を含むと、HTMLの構造を壊して任意の属性やスクリプトを差し込めるからです。esc_html() も esc_attr() も、内部では < > & " ' を実体参照に変えます。二重変換を避ける挙動(& を &amp; にしない)も同じです。
| 出力先 | 使う関数 | 例 |
|---|---|---|
| タグの間のテキスト | esc_html() |
<h2><?php echo esc_html( $title ); ?></h2> |
| 属性値 | esc_attr() |
<input value="<?php echo esc_attr( $v ); ?>"> |
| href / src | esc_url() |
<a href="<?php echo esc_url( $url ); ?>"> |
| インラインJS | esc_js() など |
可能なら wp_localize_script などで渡す |
| textarea の中身 | esc_textarea() |
<textarea><?php echo esc_textarea( $t ); ?></textarea> |
| 一部のタグを許可 | wp_kses() / wp_kses_post() |
許可リストで通す |
手順(サンプルコード)
次のコードは、フォームの値と見出しを出力する典型例です。
<?php
$title = get_option( 'myplug_title', '' );
$url = get_option( 'myplug_link', '' );
$type = get_option( 'myplug_type', 'primary' );
?>
<h2><?php echo esc_html( $title ); ?></h2>
<input type="text" name="title" value="<?php echo esc_attr( $title ); ?>">
<a href="<?php echo esc_url( $url ); ?>" class="<?php echo esc_attr( 'btn btn-' . $type ); ?>">詳細</a>
クラス名のように「固定文字列+変数」で属性を作るときは、上の esc_attr( 'btn btn-' . $type ) のように連結後の全体を渡します。
なお、翻訳文字列を出すときは esc_html__() や esc_attr_e() のように、翻訳とエスケープが一体になった関数があります(esc_html__() の使い方|説明・引数・注意点、esc_html_e() の使い方|説明・引数・注意点)。
動作確認(検証環境と結果)
環境は WordPress 7.1.2(ja)、PHP 8.2.12、MariaDB 10.4.32 です。php run.php esc-html-vs-esc-attr.php で実行しました。
esc_html : He said "hi" & <b>x</b>
esc_attr : He said "hi" & <b>x</b>
GOOD : <input value="a" onmouseover="alert(1)">
already-encoded esc_html: Tom & Jerry
esc_attr(href)= javascript:alert(1)
esc_url(href) = []
esc_url(http) = https://example.com/?a=1&b=2
分かったことは次のとおりです。
- 同じ入力に対し、
esc_html()とesc_attr()は同じ結果でした。 "を含む入力でも、エスケープ後は属性の外へ出られません(GOODの行)。esc_attr()はjavascript:alert(1)をそのまま通しました。一方、esc_url()は空文字を返しました。href に入れる値はesc_url()が必須です。wp_kses()でaとstrongだけ許可した場合、onclickやスクリプトの属性は消え、javascript:の href も無害化されました。ただし<script>の中身の文字列は、テキストとして残りました(タグは消えるが、中身は消えない点に注意)。sanitize_text_field()は、タグ・改行・前後の空白・%abのようなパーセント記号の並びを除去しました。
esc_html と esc_attr の違いの注意点
- 「悪い例」として、次のコードは
"で属性を閉じられます。そのままコピーしないでください。
// 悪い例: エスケープなし。onmouseover を差し込まれる
echo '<input value="' . $_GET['q'] . '">';
esc_html()をechoする前に二重でかけると、&amp;のような表示崩れが起きることがあります。保存時にHTMLエンティティへ変換しない(生の文字列で保存する)のが基本です。wp_kses()は許可リスト方式です。許可する属性が多いほどリスクも増えます。- サニタイズ(
sanitize_text_fieldなど)は「保存前に形を整える」処理で、出力時のエスケープの代わりにはなりません。
esc_html と esc_attr の違いでよくあるミス
- 属性に
esc_html()を使う(動くことが多いので気づきにくい)。 - href や src に
esc_attr()を使い、javascript:を通してしまう。 - 連結する前の部品ごとにエスケープして、連結後に別の値を足してしまう。
- テンプレートの外で先にエスケープして変数に入れ、どこで済んだか分からなくなる。迷うときは、変数名に
_escapedを付けると分かりやすいです。
esc_html と esc_attr の違いのチェックリスト
- すべての
echoに、出力先に合ったエスケープが付いているか。 - URL は
esc_url()、保存用のURLはesc_url_raw()を使っているか。 - 許可するHTMLは
wp_kses()の許可リストで絞っているか。 - 保存時のサニタイズと出力時のエスケープを、別々に実装しているか。
esc_html と esc_attr の違いのFAQ(よくある質問)
Q. esc_html と esc_attr は、結局どちらを使っても同じですか?
A. 今回の環境では出力が同じでした。ただし将来の仕様や、フィルター(esc_html / attribute_escape)の差を考え、文脈どおりに使い分けます。
Q. 値が数値なら、エスケープは不要ですか?
A. 数値と分かっているなら absint() などで整数化してから出す方法もあります。迷うなら esc_attr() を付けておけば安全側です。
Q. wp_kses_post は何が許可されますか?
A. 投稿本文で使える程度のタグを許可するラッパーです。詳細は公式の wp_kses_allowed_html( 'post' ) で確認してください(wp_kses_post() の使い方|説明・引数・注意点)。
筆者の見解(esc_html と esc_attr の違い)
エスケープは「迷ったらかける」より、「出力先の名前で関数を選ぶ」と覚える方が、レビューもしやすいと考えます。esc_html と esc_attr が同じ結果になるのは事実ですが、それに甘えて混ぜるとコードの意図が読めなくなります。属性か本文かをコードの上で言い切ることが、後から読む人への最低限の説明になると思います。
esc_html と esc_attr の違いの関連項目
- esc_html() の使い方|説明・引数・注意点
- esc_attr() の使い方|説明・引数・注意点
- esc_url() の使い方|説明・引数・注意点
- wp_kses() の使い方|説明・引数・注意点
- sanitize_text_field() の使い方|説明・引数・注意点
