インジェクション: 入力がコードになるとき
インジェクションは最も古く、いまだに最もありふれた深刻なバグだ。ユーザー由来のデータが、データではなく命令として扱われたときに起こる。ほとんどのアプリにとって重要なのは2種類だ。
injection ——SQL(ストラクチャード・クエリ・ランゲージ。アプリがデータベースにデータを問い合わせるために使う言語だ)——は、ユーザー入力がデータベースクエリに紛れ込むことだ。典型的な脆弱パターンはこうだ。
// VULNERABLE: ユーザーの入力がそのままクエリに貼り付けられている
app.get("/user", (req, res) => {
const name = req.query.name;
db.query(`SELECT * FROM users WHERE name = '${name}'`);
});
// もし誰かが name = '; DROP TABLE users; -- を渡すと
// クエリはテーブルを削除するコマンドになってしまう。
修正方法はパラメータ化クエリ(プリペアドステートメントとも呼ばれる)だ。クエリとデータを別々に送るので、データベースが一方を他方と取り違えることがない。
// SAFE: 値はパラメータとして渡され、決してコードとして扱われない
app.get("/user", (req, res) => {
const name = req.query.name;
db.query("SELECT * FROM users WHERE name = ?", [name]);
});
ルールはこうだ。クエリを文字列連結で組み立ててはいけない。 diffの中で、変数の周りにSQLを組み立てるバッククォートや + を見かけたら、手を止めてパラメータ化版を要求すること。
二つの経路は同じユーザー入力をデータベースに入れますが、データベースがその入力をコマンドと取り違えないようにするのは、片方だけです。
ユーザー入力:'; DROP TABLE users; --
│
├─────────────────────────┬───────────────────────────┐
▼ ▼
✗ 文字列連結 ✓ パラメータ化
"...WHERE name= "...WHERE name = ?", [input]
'" + input + "'" (クエリとデータを分離送信)
│ │
▼ ▼
┌───────────────┐ ┌───────────────┐
│ データベース │ │ データベース │
│入力を命令とし │ │入力をデータと │
│て読む │ │してのみ扱う │
│ → テーブル消失│ │ → 安全な照会 │
└───────────────┘ └───────────────┘
侵害 安全
XSS (cross-site scripting) は、ブラウザにおける同じ発想だ。ユーザー入力を受け取って、それを生のHTML(ハイパーテキスト・マークアップ・ランゲージ。ブラウザに何を表示するかを伝えるコードだ)としてページに落とし込むと、攻撃者は <script> タグを注入でき、それがあなたの他のユーザーのブラウザで実行される——たとえばセッションを盗むために。修正方法はエスケープだ。ユーザーのコンテンツをHTMLではなくテキストとしてレンダリングする。Reactのようなモダンなフレームワークはデフォルトでエスケープしてくれる。これは素晴らしいことだが、AIが「動くようにする」ために dangerouslySetInnerHTML や innerHTML のような抜け道に手を伸ばすまでの話だ。それらは保護をバイパスする。diffの中でそうした呼び出しを見かけたら、疑うべきものとして扱うこと。
あらゆるインジェクションバグの背後にある統一原理はこうだ。データとコードを分離し続けること。 ユーザー入力がクエリ・コマンド・テンプレート・HTMLに渡るときは必ず、何かがそれをエスケープするかパラメータ化しなければならない。