SQLインジェクション入門:仕組みと対策
なぜSQLインジェクションは起きるのか
この回でやること
SQLを文字列として組み立てると、値のつもりのものが構文として読まれる。この一点をコードで確かめます。
- 読む 約 8 分
- 最後にクイズ 1 問
このレッスンで学ぶこと
SQLインジェクションは、特別に手の込んだ攻撃ではなく、SQLの組み立て方そのものから生まれます。この回はプログラムが入力を文字列としてつなぐところを見て、その穴がどこで開くのかを確かめます。
読み終えるころには、この穴が「うっかりのバグ」ではなく作りから来る構造だと、自分の言葉で説明できるようになります。
文字列としてSQLを組み立てるとどうなるか
多くのアプリは、利用者の入力を文字列としてつなぎ、1本のSQLを作ります。次は、名前で利用者を探すプログラムの骨組みです。input に入力が入り、それを連結して query を組み立てています。
SQL クエリ
input = 利用者が入力した名前
query = "SELECT * FROM users WHERE name = '" + input + "'"田中 と入力されれば、組み立てられるSQLは意図どおりです。
SQL クエリ
SELECT * FROM users WHERE name = '田中'ところが ' OR '1'='1 と打たれると、同じ組み立て方が別物を作ります。
SQL クエリ
SELECT * FROM users WHERE name = '' OR '1'='1'入力のシングルクォートが、開いていた文字列を途中で閉じました。続く OR '1'='1' は、もう名前ではなく条件式として読まれます。常に真になる条件が足され、全員分の行が返ります。
ログイン画面のように条件が2つ並ぶ場合は、もう一手が要ります。OR を足すだけでは後ろのパスワードの条件が生き残って通りません。そこを -- から行末までのコメントで切り落とす、という具体は、認証を崩す回でまとめて扱います。
文字列を閉じる、条件を足す、残りを消す。手口の名前がいくつ増えても、やっていることはこの3つの組み合わせです。
値のつもりが命令として読まれる
書いた人にとって input は名前という「値」でした。しかしデータベースは、渡された文字列を丸ごとSQLとして解釈し、どこからが値でどこからが命令かを区別しません。
'田中' と '' OR '1'='1' は、データベースに届いた時点ではどちらも同じ1本の文の一部です。ここからが入力だという印は、組み上がった文のどこにも書かれていません。だから「入力だけを特別扱いする」という直し方は、この段階では原理的に取れません。
これがSQLインジェクションの正体です。
入力の検査では、なぜ足りないのか
一見効きそうですが、原理的に穴が残ります。理由は次のとおりです。
- 数値の文脈にはクォートが要らない。
WHERE id =の後ろに数値を直接つなぐ作りに1 OR 1=1を渡すと、WHERE id = 1 OR 1=1と組み上がります。クォートが1つも無いまま条件が差し込めるので、クォートを弾く検査は素通りします - データベースごとに方言がある。 コメントやエスケープの記法が製品ごとに違い、片方向けの禁止リストは別の製品ですり抜けます
- 文字は姿を変えて届く。 URLエンコードや文字コードの扱いで、検査を通ったあとにクォートへ戻ることがあります
弾く文字を足し続けても後追いにしかなりません。SQLを文字列として組み立てている限り、入力をどれだけ疑っても境界のなさは残ります。
直す方向は「値と命令を分ける」
効くのは検査を厳しくすることではなく、値を命令の組み立てに参加させないことです。SQLの骨組みを先に固め、入力はあくまで値としてデータベースへ別に手渡す。すると入力に何が入っていても構文としては読まれず、連結で消えた値と命令の境界が、渡し方の側で引き直されます。
たとえばログインなら、骨組みと値を分けて渡すとこう書けます。
SQL クエリ
SELECT name, role FROM users WHERE name = ? AND password = ?? の場所には、入力がどんな文字でも1つの値としてしか入りません。admin' -- を渡しても、admin' -- という名前の利用者を探すだけで、コメントにも条件にもならず、正しいパスワード無しでは入れません。その仕組みの中身は、のちの回でプレースホルダとして扱います。
この章の最後の演習では、ここで見た admin' -- を実際に打ち込み、正しいパスワード無しでログインが通ることを自分の手で確かめられます。試すときは必ず自分が権限を持つ練習用の環境だけで行い、他人のサイトで確かめてはいけません。