はじめに
「ユーザー入力はサニタイズしておいてください」
現場では、よく聞く言葉です。
ただ、この一言は便利な反面、かなり危うい言葉でもあります。
なぜなら、サニタイズという言葉の中に、入力チェック、エスケープ、出力エンコード、HTMLサニタイズ、SQLのパラメータ化など、性質の違う対策がまとめて押し込まれてしまうからです。
その結果、次のような実装が生まれます。
const safe = input.replace("'", "''");
const sql = `SELECT * FROM users WHERE name = '${safe}'`;
一見、危険な文字を処理しているように見えます。しかし、これには2つの大きな問題があります。
1つ目は、言語仕様の勘違いによるエスケープ漏れ(致命的な脆弱性)です。JavaScriptの replace は最初の1文字しか置換しないため、' OR '1'='1 のように複数のクォートを含む入力で簡単に突破されてしまいます(独自サニタイズにはこうした罠がつきまといます)。
2つ目は、仮に replaceAll などで正しく全置換できたとしても、そもそもSQLインジェクション対策として「筋が悪い」ということです。OWASPはSQLインジェクション対策として、文字列連結による動的クエリを避け、プリペアドステートメント(パラメータ化クエリ)を使うことを主要な防御策として整理しています1。
本稿では、サニタイズとサニタイザーを入口にして、インジェクション対策を「入力」「処理」「出力」の3層で整理します。
1. サニタイズとは何か
サニタイズは、直訳すれば「衛生的にする」「無害化する」という意味です。
セキュリティ文脈では、危険な入力や出力を、後続の処理で安全に扱える形へ変換・除去することを指す場合が多いです。
ただし、ここで重要なのは、何に対して安全なのかは文脈で変わるという点です。
同じ文字列でも、置かれる場所によって意味が変わります。
| 文脈 | 危険になる例 | 主な対策 |
|---|---|---|
| SQL文 | 入力がSQL構文として解釈される | パラメータ化クエリ |
| HTML本文 | 入力がHTMLタグとして解釈される | HTMLエンコード |
| HTML属性 | 入力が属性値やイベントとして解釈される | 属性文脈に応じたエンコード |
| JavaScript | 入力がスクリプトとして解釈される | JS文脈に応じたエンコード、設計見直し |
| OSコマンド | 入力がコマンド引数や制御文字として解釈される | OSコマンド実行の回避、安全なAPI利用 |
| LDAP検索 | 入力がLDAPフィルタとして解釈される | LDAP用エスケープ |
OWASPは、インジェクションを「信頼できないデータがインタプリタへ送られる」ことで発生する問題として説明しており、対象はSQLだけでなく、LDAP、XPath、OSコマンド、プログラム引数などにも広がります2。
つまり、サニタイズとは「危険そうな文字を消すこと」ではありません。
本質は、データが命令として解釈されないように、文脈に応じて扱いを分けることです。
ここを混同すると、次のような危険な設計になります。
| 誤解 | 問題 |
|---|---|
' を消せばSQLインジェクションは防げる |
DBMS、文字コード、クエリ構造、エスケープ漏れに依存する |
<script> を消せばXSSは防げる |
イベント属性、URLスキーム、SVG、DOM操作など別経路が残る |
| フロントで入力制限すれば十分 | リクエストはブラウザ以外から直接送れる |
| サニタイザーを通せば全部安全 | サニタイザーの対象外文脈では安全とは限らない |
仮説として、現場で起きる事故の一部は、サニタイズという言葉が広すぎることに起因していると考えられます。
「サニタイズしてあるか」ではなく、「どの文脈で、何を、どの手段で安全化しているか」を見る必要があります。
2. インジェクションは「データが命令になる」問題
インジェクションの本質は、入力値がデータではなく命令として解釈されることです。
SQLインジェクションを例にします。
悪い例です。
const name = req.query.name;
const sql = `
SELECT id, name, email
FROM users
WHERE name = '${name}'
`;
const result = await client.query(sql);
この実装では、name がSQL文の一部として結合されています。
本来は検索条件の値であるべき入力が、SQL構文そのものに混ざっています。
安全な方向の例です。
const name = req.query.name;
const sql = `
SELECT id, name, email
FROM users
WHERE name = $1
`;
const result = await client.query(sql, [name]);
この例では、SQLの構造と値を分離しています。
$1 のようなプレースホルダ記法は利用するDBドライバによって異なりますが、重要なのは、SQL文の骨格を先に決め、値を後からバインドすることです。
OWASPは、パラメータ化クエリではデータベースがコードとデータを区別できるため、攻撃者がSQLコマンドを入力してもクエリの意図を変えられない、と説明しています1。
ここで「サニタイズ」という言葉を雑に使うと、次のようなレビューになります。
SQLに入れる前にサニタイズしていますか?
しかし、本来見るべき観点はこうです。
SQLを文字列連結で組み立てていませんか?
パラメータ化クエリを使っていますか?
テーブル名やカラム名など、パラメータ化できない箇所は許可リストで制御していますか?
この違いは大きいです。
文字列の危険文字を消す発想では、攻撃パターンとのいたちごっこになります。
一方、SQLの構造と値を分ける発想では、入力値が命令側へ混ざる経路を設計として閉じられます。
3. XSSでは「出力時の文脈」が重要になる
XSS、つまりクロスサイトスクリプティングもインジェクションの一種です。
OWASPは、XSSを「悪意あるスクリプトが、本来は信頼されているWebサイトに注入される攻撃」と説明しています3。
SQLインジェクションが「DBに対する命令化」だとすれば、XSSは「ブラウザに対する命令化」です。
例えば、コメント欄に入力された文字列を画面へ表示するケースを考えます。
<div>
ユーザーのコメントがここに入ります
</div>
このとき、ユーザー入力をHTMLとして解釈させたいのか、単なるテキストとして表示したいのかで対策が変わります。
単なるテキストとして表示したいなら、基本は出力エンコードです。
< や > などがHTMLタグとして解釈されないように、HTML文脈に合わせて変換します。
一方、ブログエディタやリッチテキストエディタのように、ユーザーに一部のHTMLを許可したい場合があります。
この場合、すべてを出力エンコードすると、太字やリンクなどの表現まで壊れてしまいます。
そこで使うのがHTMLサニタイザーです。
OWASPは、ユーザーがHTMLを作成する必要がある場合には、危険なHTMLを取り除いて安全なHTML文字列を返すHTMLサニタイズを使う、と整理しています4。また、HTMLサニタイズの例としてDOMPurifyを推奨しています4。DOMPurifyはHTML、MathML、SVGを対象にしたXSSサニタイザーとして公開されています5。
Reactを例にすると、通常のテキスト表示ではフレームワーク側のエスケープに乗るのが基本です。
type Props = {
comment: string;
};
export function Comment({ comment }: Props) {
return <div>{comment}</div>;
}
一方、HTMLとして表示する必要がある場合は、サニタイザーを通したうえで、危険なAPIの利用箇所を局所化します。
import DOMPurify from "dompurify";
type Props = {
html: string;
};
export function SafeHtml({ html }: Props) {
const sanitized = DOMPurify.sanitize(html);
return (
<div
dangerouslySetInnerHTML={{ __html: sanitized }}
/>
);
}
ここでのポイントは、DOMPurifyを使えば何でも安全、ではないことです。
安全性は、許可するタグ、属性、URLスキーム、利用するDOM API、フレームワークの仕様、ライブラリの更新状態に依存します。
つまり、XSS対策では「入力時にサニタイズしたから終わり」ではありません。
最終的にブラウザへ出力する瞬間の文脈で安全化する必要があります。
4. サニタイザー導入前に見るべき設計
サニタイザーは有効な道具です。
ただし、最初に選ぶべき道具とは限りません。
セキュリティ設計としては、次の順番で考えると整理しやすくなります。
| 優先 | 観点 | 例 |
|---|---|---|
| 1 | 危険な処理を避ける | OSコマンドを直接呼ばず、言語やライブラリのAPIを使う |
| 2 | 構造と値を分離する | SQLはパラメータ化クエリにする |
| 3 | 入力を許可リストで検証する | IDは数値、日付はISO形式、種別は定義済み値のみ |
| 4 | 文脈に応じて出力エンコードする | HTML本文、HTML属性、URL、JavaScriptで分ける |
| 5 | 必要な場合だけサニタイザーを使う | ユーザー投稿HTMLをDOMPurifyで制御する |
| 6 | 多層防御を加える | CSP、権限最小化、監査ログ、WAFなど |
IPAの「安全なウェブサイトの作り方」でも、SQLインジェクション、OSコマンド・インジェクション、クロスサイト・スクリプティング、HTTPヘッダ・インジェクション、メールヘッダ・インジェクションなど、複数の脆弱性が整理されています6。
この並びを見るだけでも、インジェクションが単一の実装ミスではなく、「入力が別の文脈で命令として扱われる」広い問題だと分かります。
特に重要なのは、許可リストです。
例えば、管理画面でソート対象のカラムを指定するケースを考えます。
悪い例です。
const sort = req.query.sort;
const sql = `
SELECT id, name, created_at
FROM users
ORDER BY ${sort}
`;
値の検索条件であればプレースホルダを使えます。
しかし、カラム名やテーブル名など、SQLの構造部分は通常の値バインドでは扱えません。
この場合は、入力値をそのままSQLへ入れるのではなく、アプリケーション側で許可リストへ写像します。
const sortKey = req.query.sort;
const allowedSortColumns = {
name: "name",
createdAt: "created_at",
};
const sortColumn = allowedSortColumns[sortKey] ?? "created_at";
const sql = `
SELECT id, name, created_at
FROM users
ORDER BY ${sortColumn}
`;
const result = await client.query(sql);
この実装では、ユーザー入力そのものはSQLへ入りません。
ユーザー入力は、アプリケーションが事前に定義した安全な候補を選ぶためのキーとしてだけ使われます。
この考え方は、SQL以外にも使えます。
| 対象 | 危険な実装 | 安全寄りの考え方 |
|---|---|---|
| ファイルパス | 入力されたパスをそのまま開く | 許可されたファイルIDから内部パスへ変換する |
| OSコマンド | 入力を含むコマンド文字列を組み立てる | コマンド実行を避け、専用APIを使う |
| リダイレクト | 入力URLへそのまま遷移する | 許可済みドメインまたは内部パスだけ許可する |
| HTTPヘッダ | 入力をヘッダ値へそのまま入れる | 改行などを拒否し、用途ごとに値を制限する |
サニタイザーを入れる前に、そもそも危険な文脈へユーザー入力を渡さない設計にできないかを検討する。
これが、実務上はかなり効きます。
5. レビューでは「サニタイズ済み」ではなく「文脈」を確認する
コードレビューや設計レビューでは、「サニタイズしていますか」という問いだけでは足りません。
次のように、文脈ごとに確認した方が精度が上がります。
| レビュー観点 | 確認すること |
|---|---|
| SQL | 文字列連結ではなく、パラメータ化クエリを使っているか |
| SQLの識別子 | テーブル名、カラム名、ソート条件を許可リストで制御しているか |
| HTML表示 | テキスト表示なら出力エンコードされているか |
| HTML許可 | HTMLを許可する理由があり、サニタイザーの設定が最小化されているか |
| JavaScript | 入力値をスクリプト文脈へ直接埋め込んでいないか |
| OSコマンド | 文字列でコマンドを組み立てていないか |
| URL |
javascript: など危険なスキームを許していないか |
| 入力検証 | 型、長さ、形式、範囲、許可値を検証しているか |
| 権限 | 仮に突破されても被害範囲が限定されるか |
| ログ | 攻撃兆候を追跡できるか |
ここで、サニタイズ、バリデーション、エンコードの役割を改めて分けます。
| 用語 | 主な目的 | 実施タイミング | 例 |
|---|---|---|---|
| 入力検証 | 想定外の値を受け取らない | 入力時 | 数値、日付、文字数、選択肢 |
| パラメータ化 | コードとデータを分ける | DB実行時 | Prepared Statement |
| エスケープ | 特殊文字の意味を打ち消す | 対象文脈へ渡す直前 | LDAP用エスケープなど |
| 出力エンコード | ブラウザに文字として解釈させる | 出力時 | HTMLエンコード |
| HTMLサニタイズ | 許可したHTMLだけ残す | HTML保存前または表示前 | DOMPurifyなど |
この分類で見ると、「サニタイズ」は万能の上位概念ではなく、対策群の一部だと分かります。
特に、SQLインジェクション対策として「サニタイズ」という言葉だけで済ませるのは危険です。
SQLでは、文字をきれいにするよりも、SQL構造と値を分離する方が本筋です。
おわりに
サニタイズという言葉は便利です。
しかし、便利すぎる言葉は、設計の粗さを隠すことがあります。
インジェクション対策で重要なのは、危険な文字を探して消すことではありません。
入力値が、SQL、HTML、JavaScript、OSコマンド、LDAPなどの文脈で、命令として解釈されないようにすることです。
実務では、次の順番で考えるのがよさそうです。
- そもそも危険な処理にユーザー入力を渡さない
- SQLはパラメータ化クエリを使う
- 構造部分に入る値は許可リストで制御する
- 画面表示は出力時の文脈でエンコードする
- HTMLを許可する必要がある場合だけサニタイザーを使う
- 権限最小化、CSP、監査ログなどで多層防御にする
次にレビューするコードで「サニタイズ済みです」という説明が出てきたら、そこで止まらずに聞いてみるとよいです。
「どの文脈に対して、何を、どの方式で安全化していますか?」
この問いに答えられる設計は、かなり強いです。
参考
-
OWASP Cheat Sheet Series: SQL Injection Prevention Cheat Sheet — SQLインジェクションの原因、プリペアドステートメント、パラメータ化クエリの根拠。(OWASP Cheat Sheet Series) ↩ ↩2
-
OWASP Cheat Sheet Series: Injection Prevention Cheat Sheet — インジェクション一般、LDAP、XPath、OSコマンドなどの整理。(OWASP Cheat Sheet Series) ↩
-
OWASP: Cross Site Scripting (XSS) — XSSの定義、Stored XSS、Reflected XSS、DOM Based XSSの整理。(OWASP Foundation) ↩
-
OWASP Cheat Sheet Series: Cross Site Scripting Prevention Cheat Sheet — 出力エンコード、HTMLサニタイズ、CSPなどの位置付け。(OWASP Cheat Sheet Series) ↩ ↩2
-
DOMPurify GitHub Repository — HTML、MathML、SVG向けXSSサニタイザーの公式リポジトリ。(GitHub) ↩
-
IPA「安全なウェブサイトの作り方」 — SQLインジェクション、OSコマンド・インジェクション、XSSなど、Webアプリケーション脆弱性対策の日本語一次資料。(ipa.go.jp) ↩
