入力検証と出力エスケープ
OWASP の Injection Prevention / XSS Prevention Cheat Sheet を取得して読み、 理解した内容を自分の言葉で書いたもの。
OWASP Top 10 の A05 Injection に対応する。
防御の順序
この順序が要点。 入力検証を先に置くと防御が崩れる。
| 順位 | 防御 | 内容 |
|---|---|---|
| 1 | 安全な API を使う | インタプリタの使用自体を避けるか、パラメータ化されたインターフェイスを使う |
| 2 | 文脈に応じてエスケープする | パラメータ化された API が使えないとき、インタプリタごとのエスケープ構文を適用する |
| 3 | 入力検証 | 許可リストによる検証と正規化。ただし完全な防御ではない |
入力検証だけでは足りない理由
許可リスト(allowlist)は拒否リスト(denylist)より堅牢だが、 どちらもパラメータ化されたクエリの代わりにはならない。
理由は、正当な機能のために特殊文字が必要になることが多いから。 名前にアポストロフィが入る、検索文字列に記号が入る。 そこを許した瞬間に、検証だけの戦略は崩れる。
「入力を綺麗にすれば安全」ではなく、使う場所で正しく扱うのが本筋。
SQL インジェクション
このリポジトリは database が厚いので、ここは特に対応させて読む。
パラメータ化クエリを最優先にする
プリペアドステートメントは SQL の構造とデータを分離する。 攻撃者がクエリの意図を書き換えられなくなる。
バインド変数を使えば、tom' or '1'='1' はそういう名前のユーザーを探す文字列として扱われ、
SQL の論理としては解釈されない。
ストアドプロシージャ
安全に実装されていれば(内部で動的 SQL を組み立てていなければ)、機能的には同等。
バインドできない場所
テーブル名、カラム名、ソートの向きはバインド変数にできない。 ここは利用者の入力を、あらかじめ定義した値へ対応づける許可リストで守る。
SQL の ORDER BY で並び順を受け取る場合などが該当する。
その他のインタプリタ
技術ごとに規則が違うので、自作のエスケープを書かずにライブラリのエンコーダを使う。
| 対象 | 注意点 |
|---|---|
| LDAP | 識別名(RFC 4514)と検索フィルタ(RFC 4515)で別の規則。ライブラリを使う |
| OS コマンド | 引数を分離して渡す形でパラメータ化する。コマンドは許可リスト、引数は & | ; $ > < などのメタ文字を除く正規表現で検証する |
| ネットワークプロトコル | SMTP・IMAP・FTP の通信ストリームへのコマンド注入を防ぐ |
XSS
まずフレームワークに任せる。ただし限界を知る
React、Angular、Vue はテンプレートと自動エスケープで守ってくれる。 問題は、その保護を回避する出口が用意されていること。
React dangerouslySetInnerHTML
Angular bypassSecurityTrustAs*
名前が警告になっている。 使うときは、そこだけ手動で守る責任を引き受けたことになる。
エンコードは文脈ごとに違う
ブラウザの解析規則が文脈ごとに違うので、エンコード方法も文脈ごとに違う。
間違った方法を使うと、脆弱性が入るか、機能が壊れる。 HTML エンティティエンコードを JavaScript の文脈に置いても守れないし、 JavaScript エンコードを HTML 本文に置くと表示が壊れる。
| 文脈 | エンコード |
|---|---|
| HTML 本文 | & → &、< → <、> → >、" → "、' → ' |
| HTML 属性 | すべての文字を &#xHH; 形式に。必ず値を引用符で囲む(囲めばエンコードすべき文字集合が大幅に減る) |
| JavaScript | \uXXXX 形式。変数は引用符で囲まれたデータ値の位置にだけ置く |
| CSS | 変数はプロパティの値の位置だけ。JavaScript からは style.property = x を使うと自動でエンコードされる |
| URL | パラメータ値をパーセントエンコード(%HH)。href に入れるならURL エンコードの後に HTML 属性エンコード |
エンコードでは守れない場所
次の場所に利用者のデータを直接置くと、出力エンコードをしても XSS を完全には防げない。
<script> タグの中
HTML コメントの中
<style> ブロックの中
JavaScript のイベントハンドラ属性
コールバック関数
javascript: プロトコルの URL
ここは「エスケープして置く」のではなく「置かない」が答え。
DOM の安全な代入先と危険な代入先
| 安全(テキストとして扱う) | 危険(コードを実行しうる) |
|---|---|
elem.textContent | innerHTML(サニタイズしない限り) |
elem.setAttribute(safeName, value) | eval() / setInterval() / setTimeout() |
formfield.value | 検証なしのイベントハンドラ属性 |
document.createTextNode() | outerHTML / document.write |
リッチコンテンツを許すとき
利用者に書式を許す必要があるなら、HTML サニタイズで危険な HTML を除去する。 OWASP は DOMPurify を推奨している。
注意が2つ。
- サニタイズ後に変更を加えると、安全性が無効になることがある
- バイパスは定期的に発見されるので、ライブラリのパッチを当て続ける
CSP と Trusted Types は多層防御
CSP は主たる防御ではなく、追加の層。
全社的に CSP を義務づけることには警告が出ている。 ブラウザの対応状況に差があり、既存アプリを壊しうるため。
Trusted Types(Chromium 系)は、innerHTML outerHTML document.write といった
DOM XSS の代入先が素の文字列を拒否するようにする。
すべての代入が検証済みのポリシーを通ることを強制でき、
DOMPurify に委譲すればDOM XSS の一群を丸ごと消せる。
やってはいけない近道
原典が明示的に否定している2つ。
- CSP ヘッダだけに頼る — ブラウザの対応差、レガシーアプリの破損
- HTTP のインターセプタやフィルタで一括処理する — そのパラメータがどの文脈で描画されるかを、フィルタは知らない。 DOM ベースの XSS も拾えず、内部 REST やデータベース由来の信頼できないデータも扱えない
「クエリパラメータが HTML の文脈で描画されるのか JavaScript の文脈なのか、 サーブレットフィルタにどうやって分かるのか」 という問いが、この近道の本質的な欠陥。
既存の記述との対応
| 既にある | ここでの位置づけ |
|---|---|
API 設計 の Content-Type と nosniff | XSS の前提条件を潰す |
| 同 JSON のエスケープ | 出力エスケープの具体例 |
| 同 ヘッダー の CSP | 多層防御の層。X-XSS-Protection は非推奨になった |
| コード の「チェック・バリデーション」 | 防御の3番目。単独では足りない |
関連
- OWASP Top 10 — A05 Injection
- 認証と認可 — A01 / A07
- SQL — パラメータ化の対象
- API 設計 — ヘッダーと JSON の具体