インジェクション: 入力がコードになるとき
インジェクションは最も古く、いまだに最もありふれた深刻なバグだ。ユーザー由来のデータが、データではなく命令として扱われたときに起こる。ほとんどのアプリにとって重要なのは2種類だ。
SQL injection は、ユーザー入力がデータベースクエリに紛れ込むことだ。典型的な脆弱パターンはこうだ。
// 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を組み立てるバッククォートや + を見かけたら、手を止めてパラメータ化版を要求すること。
XSS (cross-site scripting) は、ブラウザにおける同じ発想だ。ユーザー入力を受け取って、それを生のHTMLとしてページに落とし込むと、攻撃者は <script> タグを注入でき、それがあなたの他のユーザーのブラウザで実行される——たとえばセッションを盗むために。修正方法はエスケープだ。ユーザーのコンテンツをHTMLではなくテキストとしてレンダリングする。Reactのようなモダンなフレームワークはデフォルトでエスケープしてくれる。これは素晴らしいことだが、AIが「動くようにする」ために dangerouslySetInnerHTML や innerHTML のような抜け道に手を伸ばすまでの話だ。それらは保護をバイパスする。diffの中でそうした呼び出しを見かけたら、疑うべきものとして扱うこと。
あらゆるインジェクションバグの背後にある統一原理はこうだ。データとコードを分離し続けること。 ユーザー入力がクエリ・コマンド・テンプレート・HTMLに渡るときは必ず、何かがそれをエスケープするかパラメータ化しなければならない。