esc_html と esc_attr の違い|使い分けと esc_url が必要な場面

結論

  • 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; を &amp;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 &quot;hi&quot; &amp; &lt;b&gt;x&lt;/b&gt;
esc_attr : He said &quot;hi&quot; &amp; &lt;b&gt;x&lt;/b&gt;
GOOD : <input value="a&quot; onmouseover=&quot;alert(1)">
already-encoded esc_html: Tom &amp; Jerry
esc_attr(href)= javascript:alert(1)
esc_url(href) = []
esc_url(http) = https://example.com/?a=1&#038;b=2

分かったことは次のとおりです。

  1. 同じ入力に対し、esc_html() と esc_attr() は同じ結果でした。
  2. " を含む入力でも、エスケープ後は属性の外へ出られません(GOOD の行)。
  3. esc_attr() は javascript:alert(1) をそのまま通しました。一方、esc_url() は空文字を返しました。href に入れる値は esc_url() が必須です。
  4. wp_kses() で a と strong だけ許可した場合、onclick やスクリプトの属性は消え、javascript: の href も無害化されました。ただし <script> の中身の文字列は、テキストとして残りました(タグは消えるが、中身は消えない点に注意)。
  5. sanitize_text_field() は、タグ・改行・前後の空白・%ab のようなパーセント記号の並びを除去しました。

esc_html と esc_attr の違いの注意点

  • 「悪い例」として、次のコードは " で属性を閉じられます。そのままコピーしないでください。
// 悪い例: エスケープなし。onmouseover を差し込まれる
echo '<input value="' . $_GET['q'] . '">';
  • esc_html() を echo する前に二重でかけると、&amp;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 の違いの関連項目

出典(一次情報)

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