Webアプリの主要セキュリティリスク一覧で触れた「インジェクション」を掘り下げます。
何が起きるのか
ログインフォームがあり、入力されたユーザー名を使って、こういう命令文(データベースへの問い合わせ)を組み立てているとします。
SELECT * FROM users WHERE name = '入力された名前'
ここで、ユーザー名の欄に、こんな文字列を入力されたとします。
' OR '1'='1
組み立てられる命令文は、こうなります。
SELECT * FROM users WHERE name = '' OR '1'='1'
'1'='1'は常に成り立つ条件なので、名前の一致条件が意味をなさなくなり、全ユーザーの情報が返ってきてしまいます。 これがSQLインジェクションです。ユーザー名という「ただのデータ」のつもりで入力された文字列が、命令文の一部として実行されてしまうことで起きます。
名前が付いているのはSQL(データベースへの命令を書くための言語)を狙ったものですが、考え方は他の場所にも応用されます。データベースがNoSQL系のものならNoSQLインジェクション、サーバー上のコマンドを実行する処理があればコマンドインジェクション、というように、「入力された文字列を、命令の一部としてそのまま使ってしまう」構造がある限り、同じ種類の穴が起こり得ます。
なぜ、多くの場合は標準の書き方だけで防げるのか
現代の多くのライブラリ・ORM(Object-Relational Mapping、データベースの表をプログラムのオブジェクトのように扱えるようにする道具。PrismaやDrizzleなどが知られています)は、データを命令文と混ぜずに、別々に渡す書き方を標準にしています。これをパラメータ化クエリ(またはプリペアドステートメント)と呼びます。
SELECT * FROM users WHERE name = ?
のように命令文の形は先に固定しておき、?の部分に渡す値はあくまでデータとして扱われ、命令の一部として解釈されることはありません。 ' OR '1'='1という文字列を渡しても、「そういう名前の人を探す」という検索として扱われるだけで、命令文の構造自体は変わりません。
危険なのは、文字列を直接つなぎ合わせて命令文を組み立てる書き方をしたときです。 ORMやライブラリの標準機能を使わず、「文字列連結で命令文を作る」処理を自分で(あるいはエージェントに頼んで)書いた場合に、この穴が生まれます。
対策の具体策
- ORMや、ライブラリの標準の書き方をそのまま使う。 多くの場合、これだけで自動的にパラメータ化クエリになります
- 文字列をつなぎ合わせて命令文を組み立てる書き方を避ける。 「動的にSQLを組み立てる必要がある」場面でも、値の部分だけは必ずパラメータとして渡す
- 入力値の形式チェックも重ねる。 メールアドレスの欄に明らかにメールアドレスでない文字列が来たら弾く、というような形式チェックは、それ自体が主対策ではありませんが、多層防御の1つとして効きます
- データベースのアカウントに、必要以上の権限を与えない。 アプリが使うデータベースの接続情報に、「全テーブルを削除できる」権限まで持たせておくと、仮に穴を突かれたときの被害が大きくなります
エージェントへの頼み方
「データベースへの問い合わせを組み立てている箇所を確認して、文字列を直接つなぎ合わせて命令文を作っている処理がないか探して。あればパラメータ化クエリに直して」のように頼むと点検が進みます。最初からORMを使う前提でアプリを作ってもらえば、そもそもこの問題に触れずに済むことも多いです。
まとめ
- SQLインジェクションは、入力された文字列が、データではなく命令の一部として実行されてしまうことで起きる
- 同じ構造の問題は、SQL以外(NoSQL、OSコマンドなど)にも起こり得る
- 多くの場合、ORMや標準のライブラリの書き方を使っているだけで自動的に防げる
- 危険なのは、文字列を直接つなぎ合わせて命令文を組み立てる書き方をしたとき
古典的な攻撃ですが、標準の道具を、標準の使い方で使うという基本を守っているだけで、多くの場合は自然と防げています。