中級では「何を・どこで・どの順で確かめるか」を整理しました。上級では、正規化の落とし穴・信頼境界の置き方・そしてなぜ多くの注入は結局「出力の問題」なのかを掘り下げます。関連は クロスサイトスクリプティング・SQLインジェクション・Webの中のセキュリティ。
上級の解説は準備中のため、上級の内容を表示しています。
概要 — まず全体をつかむ
詳細 — 1段階ずつ追う
許可リスト設計と拒否リストの限界
「危ないものを弾く」拒否リストは、攻撃者の発想力との追いかけっこになります。設計思想として弱いのは、次の非対称性のためです。
- 拒否リストは「既知の悪い形」を数え上げる。新しい表現・エンコード・記号の組み合わせが出るたびに穴が空く。守る側が全パターンを先回りするのは原理的に難しい
- 許可リストは「既知の良い形」だけを通す。想定外はすべて弾くので、未知の攻撃表現も自動的に落ちる
- 実務では構造化して絞るのが強い。「英数字4〜32文字」より「
^[a-z0-9_]{4,32}$に一致」、さらに「区分値は列挙した候補のいずれか」——形式・集合・型で閉じるほど、すり抜けの余地が消える ※1
アニメーション『入力検証=関所』を開く
正規化(canonicalization)の罠
同じ意味の入力が、複数の見た目で書けることが問題の温床です。検証の前に一意な正規形へ揃えるのが正規化です ※2。揃える前に検証すると、通った後で別の形へ「化けて」すり抜けられます。
- Unicode正規化 — 合成済み文字と結合文字列(例:
éの2通りの書き方)を NFC などで統一する。揃えないと、同じ見た目が別バイト列として検証をすり抜ける - 同形異義(homoglyph) — キリル文字の
аとラテンのaは見た目が同じ。ドメインや識別子のなりすましに使われる - パストラバーサル —
../でディレクトリを遡る手口。....//や%2e%2e%2fなど変形が多く、パスを絶対パスへ解決してから許可された基準ディレクトリの内側かを確かめる - 二重エンコード —
%252eは、1回デコードで%2e、もう1回で.になる。途中で1回だけデコードして検証すると、後段の処理でもう一段デコードされて危険な形に戻る。デコードは回数と順序を決め、正規化しきってから検証する
アニメーション『攻撃は境界で起きる』を開く
検証・サニタイズ・出力エンコードは別concern
三つは似て見えて役割が違います。混同すると「片方だけで安心」する事故が起きます。
- 検証(入口) — 想定した「形」かを確かめ、外れたら弾く。値の中身は変えない。関所
- サニタイズ — 危険な部分を除去・加工して受け入れる。形をゆがめる副作用があるので、検証で弾けるものは弾くのが優先
- 出力エンコード(出口) — 通した値を、出す先の文脈(HTML・SQL・URL・シェル)に合わせて無害化する。ここが本丸
要点は、検証は「その先どこで使うか」を知らないということ。だから入口でどれだけ厳しく検証しても、出口の無害化を省くと危険になりうる。両者は別concernとして両方やります。
信頼境界で検証する
「どこで検証するか」は「誰が権威か」の問題です。
- サーバ側が権威 — 検証は信頼境界の内側、つまりサーバで必ず行う ※3。クライアント検証は書き換えられるので、攻撃者はフロントを無視して直接APIへ送れる
- クライアント検証はUX — 入力ミスへの即時フィードバックという親切機能であって、防御ではない。UXのために置くが、防御としては数えない
- 各層で再検証 — フロント → APIゲートウェイ → サービス → DB と値が渡るなら、信頼境界を越えるたびに確かめる。上流を通ったからと下流が検証を省くと、内部から来た不正値を素通しする
- サーバ間も境界 — 別サービスからの入力も「外」。マイクロサービスでは、隣が検証済みだろうという前提を置かない
スキーマ検証
APIのリクエストボディのような構造化データは、フィールドごとの手続き検証より、スキーマで宣言的に縛るほうが漏れにくい。
- 各フィールドの型・必須/任意・長さ・範囲・列挙値・正規表現をスキーマとして一枚に定義し、境界で機械的に照合する
- 未知フィールドは拒否(strict)にして、想定外のキーが内部へ流れ込むのを止める
- スキーマは「許可リストを構造化した姿」。仕様書と検証が一致するので、実装のすり抜けが減る
なぜ多くの注入は「出力の問題」に帰着するか
- 注入=混ざること — XSS も SQLインジェクションも、根っこは「データが、出力先の文法で命令として解釈される」こと。HTMLならスクリプトタグ、SQLなら命令の切れ端。つまり危険が生まれるのは出力の瞬間
- 入口検証だけでは閉じない — 入口で形を絞っても、出力先ごとの危険記号まで入口では消しきれない(メールアドレスにも
<は入りうる)。だから根治は出力の文脈に合わせた無害化——XSS なら文脈別エスケープ、SQLインジェクション ならパラメータ化 - 検証は減災、無害化は根治 — 入口の検証は攻撃面を狭める価値があるが、出口の無害化を代替はしない。検証で表面を減らし、出力の文脈で確実に無害化する——この二段構えが要
- 正規化忘れ・順序ミス — 正規化を後回しにすると、検証を通った後で化けてすり抜けられる。正規化しきってから検証する
登場人物メモ:
- ※1 許可リスト — 通してよいものだけを列挙し、それ以外はすべて弾く方式。想定外に強い
- ※2 正規化(canonicalization) — 同じ意味の複数表記を、一意な正規形へ揃える前処理
- ※3 信頼境界 — 信用できる領域とできない領域の境目。検証はこの境界を越える所で行う
理解度チェック
そのまま解けます(成績は保存されません)。無料アカウントを作ると、学習の記録と進捗の山登りが始まります。
問1. 入力検証を通った値が、それでも出力先で危険になりうるのはなぜか?
問2. 二重エンコードやUnicodeの同形異義を悪用した「すり抜け」を防ぐために、検証の前にすべきことは?
問3. 「通してよいものだけを列挙し、それ以外はすべて弾く」検証方式を何と呼ぶ?
問4. 許可リスト方式が拒否リスト方式より「未知の攻撃にも強い」といえる根拠はどれ?
問5. 危険な部分を除去して受け入れる「サニタイズ」と「検証」の使い分けとして本文が勧めるのは?
問6. APIのリクエストボディをスキーマで検証するとき、想定外のキー(未知フィールド)への安全な既定はどれ?
問7. フロント → APIゲートウェイ → サービス → DB と値が渡る構成で、各層が検証すべき理由として本文に沿うのは?
問8. 合成済み文字と結合文字列(é の2通りの書き方)を一つに揃えるUnicode正規化形を、英字3文字で答えてください。