入力検証と出力エスケープ

OWASP の Injection Prevention / XSS Prevention Cheat Sheet を取得して読み、 理解した内容を自分の言葉で書いたもの。

OWASP Top 10A05 Injection に対応する。

防御の順序

この順序が要点。 入力検証を先に置くと防御が崩れる。

順位防御内容
1安全な API を使うインタプリタの使用自体を避けるか、パラメータ化されたインターフェイスを使う
2文脈に応じてエスケープするパラメータ化された API が使えないとき、インタプリタごとのエスケープ構文を適用する
3入力検証許可リストによる検証と正規化。ただし完全な防御ではない

入力検証だけでは足りない理由

許可リスト(allowlist)は拒否リスト(denylist)より堅牢だが、 どちらもパラメータ化されたクエリの代わりにはならない。

理由は、正当な機能のために特殊文字が必要になることが多いから。 名前にアポストロフィが入る、検索文字列に記号が入る。 そこを許した瞬間に、検証だけの戦略は崩れる。

「入力を綺麗にすれば安全」ではなく、使う場所で正しく扱うのが本筋。

SQL インジェクション

このリポジトリは database が厚いので、ここは特に対応させて読む。

パラメータ化クエリを最優先にする

プリペアドステートメントは SQL の構造とデータを分離する。 攻撃者がクエリの意図を書き換えられなくなる。

バインド変数を使えば、tom' or '1'='1'そういう名前のユーザーを探す文字列として扱われ、 SQL の論理としては解釈されない。

ストアドプロシージャ

安全に実装されていれば(内部で動的 SQL を組み立てていなければ)、機能的には同等。

バインドできない場所

テーブル名、カラム名、ソートの向きはバインド変数にできない。 ここは利用者の入力を、あらかじめ定義した値へ対応づける許可リストで守る。

SQLORDER BY で並び順を受け取る場合などが該当する。

その他のインタプリタ

技術ごとに規則が違うので、自作のエスケープを書かずにライブラリのエンコーダを使う。

対象注意点
LDAP識別名(RFC 4514)と検索フィルタ(RFC 4515)で別の規則。ライブラリを使う
OS コマンド引数を分離して渡す形でパラメータ化する。コマンドは許可リスト、引数は & | ; $ > < などのメタ文字を除く正規表現で検証する
ネットワークプロトコルSMTP・IMAP・FTP の通信ストリームへのコマンド注入を防ぐ

XSS

まずフレームワークに任せる。ただし限界を知る

React、Angular、Vue はテンプレートと自動エスケープで守ってくれる。 問題は、その保護を回避する出口が用意されていること。

React    dangerouslySetInnerHTML
Angular  bypassSecurityTrustAs*

名前が警告になっている。 使うときは、そこだけ手動で守る責任を引き受けたことになる。

エンコードは文脈ごとに違う

ブラウザの解析規則が文脈ごとに違うので、エンコード方法も文脈ごとに違う。

間違った方法を使うと、脆弱性が入るか、機能が壊れる。 HTML エンティティエンコードを JavaScript の文脈に置いても守れないし、 JavaScript エンコードを HTML 本文に置くと表示が壊れる。

文脈エンコード
HTML 本文&&amp;<&lt;>&gt;"&quot;'&#x27;
HTML 属性すべての文字を &#xHH; 形式に。必ず値を引用符で囲む(囲めばエンコードすべき文字集合が大幅に減る)
JavaScript\uXXXX 形式。変数は引用符で囲まれたデータ値の位置にだけ置く
CSS変数はプロパティの値の位置だけ。JavaScript からは style.property = x を使うと自動でエンコードされる
URLパラメータ値をパーセントエンコード(%HH)。href に入れるならURL エンコードの後に HTML 属性エンコード

エンコードでは守れない場所

次の場所に利用者のデータを直接置くと、出力エンコードをしても XSS を完全には防げない。

<script> タグの中
HTML コメントの中
<style> ブロックの中
JavaScript のイベントハンドラ属性
コールバック関数
javascript: プロトコルの URL

ここは「エスケープして置く」のではなく「置かない」が答え。

DOM の安全な代入先と危険な代入先

安全(テキストとして扱う)危険(コードを実行しうる)
elem.textContentinnerHTML(サニタイズしない限り)
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 と nosniffXSS の前提条件を潰す
JSON のエスケープ出力エスケープの具体例
ヘッダー の CSP多層防御の層。X-XSS-Protection非推奨になった
コード の「チェック・バリデーション」防御の3番目。単独では足りない

関連