先に要点
- エスケープ は 「出力先の文法に合わせて、特別な意味を持つ文字を無効化する処理」。出力先(HTML / SQL / シェル / JS / JSON / URL)ごとにルールが違う ことを理解せず、「htmlspecialchars さえすれば安全」 と思うのが事故のもと。
- HTML 出力 → テンプレートエンジンの自動エスケープに任せる。SQL → プレースホルダ(prepared statement)。シェル → シェルを介さない API + 引数配列。JS / JSON → JSON.stringify や専用エンコーダ。URL → encodeURIComponent / urlencode。「場所ごとに正解が違う」 と覚える。
- サニタイズ は 「危険要素を取り除く加工」、エスケープは 「相手の言語に翻訳する」。エスケープは 出力直前 にやり、保存値は変更しないのが原則。
- テンプレートエンジン(Blade / React JSX / Vue / Twig / Jinja)の 自動エスケープを切らずに使う だけで、XSS の大半は防げる。「{!! !!}」 や 「dangerouslySetInnerHTML」 のような 自分で自動エスケープを切るシンタックス を使う箇所だけ要注意。
- 同じ画面で 複数のコンテキストが混ざる場合(「HTML 属性に JS を埋め込む」 「JS リテラル中に HTML を埋め込む」)は、外側 → 内側の順でそれぞれエスケープが必要。「二段階のコンテキスト」 を見落とすのが上級者でも事故るパターン。
「PHP の htmlspecialchars をかけたから XSS は大丈夫」 「エスケープしてから DB に入れている」 ── どちらも実は 用語の取り違え です。エスケープは 出力先(HTML、SQL、シェル、JS、URL...)ごとにルールが違う ため、「どこに出すか」 を意識せずに一律でかけると、「守れていない」 か 「必要な値が壊れる」 のどちらかになります。
この記事では、エスケープを 出力先の文法に合わせた翻訳作業 と捉え直し、出力先ごとの正しい作法と、テンプレートエンジン任せにできる境界線を整理します。サニタイズ・エスケープ・バリデーションの違い の続編として、エスケープ深掘りの位置づけです。
エスケープとは — 「相手の言語に翻訳する」
エスケープは 出力先で特別な意味を持つ文字を、特別な意味を持たない形に変換する処理 です。「元の値を壊すのではなく、出力先の文法で安全な表現に翻訳する」 のがポイントです。
HTML 出力でのエスケープ
不等号や山括弧をそのまま書くとタグとして解釈されるので、「 <」 「>」 のような実体参照に翻訳する。「スクリプトタグ + 任意コード + 閉じスクリプトタグ」 と入っていた文字列が、ブラウザでは ただの文字列として表示される。
SQL 出力でのエスケープ
「 シングルクォート」 や 「;」 が SQL の文法的な区切りになるため、プレースホルダで値と文を分離するのが現代的な答え。手書きエスケープは Charset 問題で抜けるので、原則使わない。
シェル出力でのエスケープ
「 ;」 や 「&&」 「バッククォート」 がコマンド区切りやコマンド置換になる。シェルを介さない API + 引数配列 で渡すのが安全。「シェル文字列を作って渡す」 設計自体を避ける。
「htmlspecialchars」 は 「HTML 出力用のエスケープ関数」 です。SQL や JS のエスケープには使えない、というのが最初に押さえるべき事実です。
出力先ごとの正しい作法
代表的な出力先ごとに、「何を使うのが正解か」 を整理します。
| 出力先 | 正解の手段 | 典型的な誤り |
|---|---|---|
| HTML テキスト | テンプレートエンジンの自動エスケープ(Blade 「{{ }}」、React JSX、Vue 「{{ }}」、Jinja 「{{ }}」) | 自分で 「str_replace」 して不等号開きだけ置換、「{!! !!}」 や 「dangerouslySetInnerHTML」 を安易に使う |
| HTML 属性 | テンプレートの属性エスケープ(「title="{{ \$x }}"」 で OK のフレームワークが多い) | 引用符を付けずに属性に埋め込む(「」)、JS イベント属性に値を埋め込む |
| SQL | プレースホルダ(「PDO」 「Eloquent」 「Prisma」 「psycopg2」 など) | 手書きエスケープ(「mysql_real_escape_string」)、文字列連結で SQL を組み立てる |
| シェル / コマンド | 「 シェル経由しない API + 引数配列」(「subprocess.run([..], shell=False)」 「execFile」 など) | 「 os.system」 「exec」 でシェル文字列を連結、自前でシェルメタ文字を置換 |
| JS 文字列リテラル | 「 JSON.stringify」 の出力をそのまま埋め込む、または専用エンコーダ | ダブルクォートだけ置換、「不等号開きを見落とす(閉じ script タグで抜けられる)」 |
| JSON | 言語標準の 「JSON.stringify」 / 「json_encode」 | 自前で文字列連結して JSON を組み立てる |
| URL クエリ | 「 encodeURIComponent」(JS) / 「urlencode」(PHP) / 「quote」(Python) | 「 encodeURI」 を 「encodeURIComponent」 と混同(エスケープする文字が違う) |
| CSS | ユーザー入力を CSS 値に埋め込まない設計。やむを得ない場合は CSS.escape 系 | 「 style=」 属性にユーザー入力を直接埋める |
| LDAP / XML / 正規表現 | それぞれの専用エスケープ関数(「ldap_escape」 「htmlspecialchars」 の XML 互換、「preg_quote」 等) | 「 HTML 用エスケープで XML をエスケープしてるつもりになる」 |
ポイントは 出力先ごとに専用の関数 or 仕組みがある ことを認識し、「一つの関数で全部」 という発想を捨てることです。
テンプレートエンジン任せにできる範囲とできない範囲
モダンな Web フレームワークのテンプレートエンジンは、HTML テキスト部分の自動エスケープ を提供しています。これだけで XSS の大半が防げます。
任せて OK な範囲
` Blade の 「{{ \$x }}」 ` `JSX の 「{x}」 ` `Vue の 「{{ x }}」 ` `Jinja の 「{{ x }}」 ` のように 通常のテキスト出力構文 を使う限り、自動的に HTML エスケープが効く。「普通のユーザー名・コメント本文・記事タイトル」 をそのまま出すだけなら十分。
任せられない: 自動エスケープを切るシンタックス
` Blade の 「{!! \$x !!}」 ` `React の 「dangerouslySetInnerHTML」 ` `Vue の 「v-html」 ` のような 開発者が明示的に自動エスケープを切る記法 を使う場所は、「HTML を許す前提のサニタイズが必要」 になる。サニタイズの記事 の範疇。
任せられない: 属性内の JS / URL
任せられない: 別言語の埋め込み
HTML 中の script タグの内側で JS 文字列リテラルとしてサーバ側の値を埋め込むパターン(「スクリプトタグの中で、JS の文字列リテラルの中にテンプレ変数を出力する書き方」)は、HTML エスケープと JS リテラルエスケープの二段階 が必要。「JSON.stringify した結果をテンプレ補間で出す」 という二段構えが安全。
「テンプレートエンジン任せで OK な部分」 と 「自分で考えて対策が必要な部分」 の境界を意識すれば、レビューでも実装でも見落としが減ります。
ネストするコンテキスト — 二段階エスケープ
実務でいちばん事故りやすいのが 1つの値が複数の文脈にネストして埋め込まれる ケースです。
HTML 中の JS 文字列リテラル
script タグの内側で 「const userName = テンプレ変数」 のような JS 文字列リテラルにサーバ値を埋める書き方では、「name」 に 「";alert(1);//」 が来ると、HTML エスケープではダブルクォートが変換されず、JS 文字列を脱出して任意コード実行。JSON.stringify した結果をテンプレ補間で挟む のが定石。
HTML 属性中の JS
`
CSS 中の URL
「 style="background-image: url({{ \$url }});"」 → URL に 「;」 や 「}」 が混ざると CSS パーサを脱出。CSS にユーザー入力を埋める設計は 原則禁止、どうしても必要なら CSP の 「style-src」 と組み合わせて厳格に。
ネストの基本原則は、外側のコンテキストから順に内側へ向かってエスケープを重ねる。「HTML 属性 → JS リテラル → 値」 のように 各段で必要なエスケープを全部かける 必要があります。
SQL のエスケープが特別な理由
SQL は 手書きエスケープがほぼ使えない 数少ない出力先です。
「プレースホルダ + 値ではない部分はホワイトリスト」 の組み合わせが、現代の SQL 安全設計の標準です。
エスケープが必要なのに忘れがちな場所
「 通常のテンプレ出力では自動エスケープが効くから」 と気を抜きがちですが、自動エスケープが効かない場所 も意外と多くあります。
エラーメッセージ / ログ表示
「 例外メッセージをそのまま画面に表示」 する開発モード画面で XSS が起きるケース。本番では詳細を出さない、開発でも 「画面表示前にエスケープ」 を徹底。
PDF / CSV 出力
CSV に 「=SUM(...)」 を入れられると Excel で開いた瞬間に数式実行(CSV インジェクション)。「=」 「+」 「-」 「@」 で始まる値は 「'」 を前置する対策が必要。
メール件名・本文
件名に 改行コード を入れられると 追加ヘッダ注入(「Bcc:」 を勝手に追加など)。メール送信ライブラリのヘッダ用関数を使い、生で文字列連結しない。
ファイル名・パス
「 ../」 や NULL バイトを含むファイル名を保存して、後で読み込み時にパストラバーサル。basename() で正規化」 `アップロード時にホワイトリスト文字種に限定。
「画面に出していないから関係ない」 ではなく、任意の 「別のシステム」 へデータが渡る瞬間 がエスケープ対象、と捉えるとモレが減ります。
実装パターンの具体例
各フレームワークでよく書く 「エスケープが効いた状態の書き方」 を整理しておくと、レビューで一発で違いを見抜けます。
テキスト出力は 「{{ \$value }}」 で常に HTML エスケープが効く。HTML を許す投稿のように 「エスケープを切りたい」 場合だけ 「{!! \$value !!}」 を使い、その直前で必ず HTML サニタイザ(HTML Purifier など)を通す。JS リテラルに値を入れたいときは Blade の 「@json(\$data)」 ヘルパで安全な JSON 文字列として出す。script タグの内側で閉じスクリプトタグの文字列が紛れ込んでも安全に処理される。
React / JSX
JSX 内の 「{value}」 はデフォルトで HTML エスケープが効く。「dangerouslySetInnerHTML」 は名前の通り危険を意識させる命名になっていて、「サニタイザを通した HTML を渡すための専用入口」 と考える。属性の URL は 「href={value}」 の値が 「javascript:」 で始まらないか別途検証する。
テンプレ補間 「{{ value }}」 と 「v-bind」 はテキスト出力としてエスケープされる。「v-html」 は React の 「dangerouslySetInnerHTML」 と同じ位置づけで、「サニタイズ済み HTML を入れる場所」 と割り切る。「v-html」 を使うコンポーネントは PR で必ず人間レビューを通す運用にしておくと事故率が下がる。
「このフレームワークだとどう書けばエスケープが効いているのか」 を パターン化して覚えておく と、コードレビューで 「ここは生で組み立てているからチェックが必要」 を秒で見抜けるようになります。逆にいえば、「どこに脱出口があるか」 を知らないままレビューをすると、「普通の書き方だから安全」 と思って通したコードが事故るパターンに陥ります。
コードレビューでエスケープを見るときの観点
エスケープ関連のレビューは、「何が書かれているか」 より 何を経由して、どこに出力されているか を追うのが本質です。次の観点で読むと、見落としが減ります。
第一に、「値が最終的にどこへ流れるか」 を辿る習慣をつけます。コントローラで受け取った値が、ビューに渡るのか、データベースに保存されるのか、メールで外部に送られるのか、API レスポンスとして返るのか。流れ着く先によって必要なエスケープが違うため、「流れの終端」 を意識しないままレビューしても安全性は判断できません。
第二に、「自動エスケープが切られている場所」 を最初に探します。テンプレートエンジンが安全に守ってくれる前提で書かれているコードの中で、明示的に自動エスケープを切っているシンタックスがあれば、その部分は必ず単独でレビュー対象になります。「他の場所は自動で守られているから、ここだけ厚く見る」 と決め打ちすれば、効率と精度の両立がしやすくなります。
第三に、「生のクエリや生のコマンドを組み立てている部分」 を探します。SQL を文字列連結で組み立てている、外部コマンドをシェル経由で呼んでいる、メールヘッダを自分で連結している、ファイルパスを入力から組み立てている、といった 「脱出口を自分で開けている箇所」 は、利便性のためにフレームワークの安全機構をすり抜けているのが普通です。「なぜここでフレームワーク標準を使わないのか」 を必ず確認し、合理的理由がなければプレースホルダや専用 API への置き換えを提案します。
第四に、「複数のコンテキストが混ざる場所」 に注意します。テキストの中に別の言語が埋め込まれているような構造を見つけたら、外側と内側の両方の文脈に対するエスケープが揃っているかを確認します。レビューで気付かないと、「単独の文脈ではエスケープされているように見えて、ネストすると抜ける」 という上級者でもハマる事故が通ってしまいます。
第五に、「出力する値の出どころ」 を確認します。完全に内部で計算された定数や、認証済みユーザーだけが書ける管理画面の入力かどうか、不特定多数の入力に由来する値かで、必要な厳しさが変わります。とくに、「管理画面入力だから少しゆるくしてある」 ような実装は、「将来一般ユーザーにも開放した瞬間に事故る」 ので、「公開された場合の安全性」 を基準に評価しておくと長く生き残るコードになります。
レビュー観点を 「テスト項目」 として持っておくと、属人化を避けて、新人メンバーでも同じ精度でエスケープを見られるようになります。「ここまで通ったから安全」 ではなく、「どこを経由してどこへ出ているか」 で判断する習慣が、長く運用するチームの品質を支えます。
サニタイズ・バリデーションとの責務分担
エスケープと サニタイズ・バリデーション は、「どこで何をやるか」 で責務が分かれます。同じ問題に対して 「エスケープでやるべきか、サニタイズでやるべきか、バリデーションでやるべきか」 を切り分けられるのが、現代の Web 開発者の標準スキルです。
バリデーションは 「入口」
「 メールアドレス形式じゃない」 「数字が範囲外」 のような そもそも受け付けないルール を入口で適用する。エスケープでは 「受け付けたけど安全に翻訳する」 ことしかできないので、「本来そもそも受けたくない」 ものはバリデーションで止める。
サニタイズは 「保存・利用前の整形」
「 HTML を許すコメント投稿」 のような場合は、保存前に サニタイザライブラリ で許可タグだけ残す。エスケープと違って 「捨てる / 整える」 の発想。「そのままだと使えないが、捨てるとサービスにならない」 入力に対する加工。
エスケープは 「出力直前の翻訳」
「 保存値はそのまま、出力先の文法に合わせて翻訳」 が原則。「 保存時にエスケープしてしまう」 と、別の出力先(CSV、メール、JSON、ログ)で逆にデコードが必要になり破綻する。「保存値を変えず、出力直前に出力先別のエスケープ」 が長期運用の基本姿勢。
3つで 「深さ」 を稼ぐ
「 バリデーション + サニタイズ + エスケープ」 をレイヤーで重ねるのは、「どれか1つが抜けても別の層で守れる」 という防御の冗長化(defense in depth)。「エスケープだけで守る」 「サニタイズだけで守る」 はどちらも片肺で、長期的には必ず事故る。
エスケープに関するよくある質問
Q. htmlspecialchars と htmlentities の違いは?
A. ` htmlspecialchars は最低限の特殊文字 (「< > & " '」) を実体参照に変換」、htmlentities は変換可能なすべての文字を実体参照に変換します。現代の Web ではほぼ常に 「htmlspecialchars」 で十分です。「htmlentities」 は出力サイズが膨れる上、UTF-8 が前提の今は実利が少ないので使う場面は限定されます。
Q. テンプレートエンジンの自動エスケープがあれば手動エスケープは不要?
A. ほぼ不要だが、自動エスケープを切るシンタックスを使う箇所と、別言語の埋め込み箇所だけ要注意 です。「{!! !!}」 「dangerouslySetInnerHTML」 のような明示的な切り替えと、script タグ内の JS リテラル、「href」 や 「onclick」 のような属性は手動対応が必要です。
Q. プレースホルダを使っているから SQL は安全ですよね?
A. 値の部分は安全です。ただし ORDER BY のカラム名」 「テーブル名」 `動的に組み立てる WHERE 句のカラム名 はプレースホルダで扱えないので、ホワイトリストで別途対策が必要です。「動的にカラムが変わる検索画面」 を作るときに見落とされがち。
Q. encodeURI と encodeURIComponent はどっち使えばいい?
A. クエリパラメータの値や URL のパス断片には encodeURIComponent、URL 全体を整形する稀な場合だけ encodeURI です。「encodeURI」 は 「?」 「&」 「#」 をエスケープしないので、「値」 に使うとパラメータを壊します。値のエスケープなら encodeURIComponent 一択 と覚えるのが安全です。
Q. JSON.stringify を使えば JS の文字列埋め込みは安全?
A. ほぼ安全ですが、HTML 中に埋め込む場合は閉じスクリプトタグで JS リテラルから抜けられる可能性があるので、追加で閉じスクリプトタグの不等号開きをバックスラッシュエスケープした書式に置換する 必要があります。Laravel の 「@json」 や Rails の 「raw json_escape」 のように フレームワークの専用ヘルパを使う のが安全。
Q. シェルコマンドのエスケープは具体的にどうやる?
A. シェルを介さない API を使う が正解です。「Python の subprocess.run(["ls", "-l", filename], shell=False)」、「Node.js の execFile("ls", ["-l", filename])」、「PHP の proc_open」 引数配列、「Go の exec.Command("ls", "-l", filename)」 のように コマンドと引数を別々に渡す と、シェルメタ文字は単なる文字列として扱われます。「escapeshellarg」 のような自前エスケープは保険程度に考えるのが安全。
Q. 多言語(国際化)文字列のエスケープで気を付けることは?
A. 文字エンコーディングを明示する のが基本です。「htmlspecialchars」 の第3引数で 「UTF-8」 を明示、「urlencode」 の前提エンコーディング、「mb_*」 関数の使用、など 暗黙の前提を作らない ことで、「サロゲートペア」 「絵文字」 「RTL 言語」 のエッジケースで壊れないコードになります。
まとめ
エスケープは 出力先の文法に合わせて、特別な意味を持つ文字を無効化する処理 です。「一つの関数で全部安全」 という魔法はなく、HTML / SQL / シェル / JS / URL / JSON / CSS で正解が違う ことを意識して使い分けるのが、現代の Web 開発の前提です。
幸い、モダンなフレームワークの自動エスケープと、SQL のプレースホルダ、シェル非経由 API、JSON.stringify、encodeURIComponent を組み合わせれば、多くのケースは仕組みが守ってくれます。残るのは 「 自動エスケープを切る場所」 「ネストするコンテキスト」 「属性内の JS / URL」 のような、開発者が意識的に判断する場所だけ に絞れます。
エスケープと サニタイズ・バリデーション の役割を分けて理解できると、コードレビューでも自分のコードでも、「なぜここで何をすべきか」 を一発で説明できるようになります。
参考リンク
- OWASP: Cross Site Scripting Prevention Cheat Sheet
- OWASP: SQL Injection Prevention Cheat Sheet
- OWASP: OS Command Injection Defense Cheat Sheet
- MDN: encodeURIComponent
- PHP Manual: htmlspecialchars