SQLインジェクションは、もう何年も前から知られている攻撃手法です。それでも実際の被害報告が絶えないのは、「知っているつもり」で実装してしまうケースが多いからです。
この記事では、SQLインジェクションがなぜ起きるのかという仕組みの部分から、実務でどう対策すればよいのかまでを整理します。
Webアプリのセキュリティ対策を一通り確認したい方は、 Webアプリの脆弱性・セキュリティ対策まとめ もあわせてご覧ください。
SQLインジェクションとは、フォームやURLパラメータなどユーザーが入力できる箇所に、意図的な文字列を送り込むことで、アプリが組み立てるSQL文の構造そのものを書き換えてしまう攻撃です。
「ユーザー名」や「検索キーワード」といった一見無害な入力欄が、そのまま攻撃の入り口になり得ます。
典型的な原因は、SQL文を文字列連結で組み立てていることです。次のようなコードを考えてみます。
SELECT * FROM users WHERE name = '入力値' AND password = '入力値'
一見普通のログイン処理に見えますが、ここに以下のような文字列を入力するとどうなるでしょうか。
' OR '1'='1' --
この文字列がそのまま埋め込まれると、SQL文は次のように変化します。
SELECT * FROM users WHERE name = '' OR '1'='1' -- ' AND password = '...'
'1'='1'は常に真になるため、WHERE句の条件が実質的に無効化されます。--以降はコメントとして扱われるため、パスワードのチェックごと無視されます。結果として、IDもパスワードも知らないまま、ログインが成立してしまいます。
これがSQLインジェクションの基本的な仕組みです。入力値が「データ」としてではなく、「SQL文の一部」として解釈されてしまうことが問題の本質です。
言葉で説明されてもピンとこないと思うので、実際に入力しながら確かめてみましょう。以下のデモは実際のデータベースには一切接続しません。入力した文字がSQL文の中でどう扱われるかを、その場で可視化するだけです。
実際のデータベースには接続しません。ユーザー名検索の入力が、SQL文の中でどう扱われるかを確認できます。
上のボタンを押すと、実行結果がここに表示されます。
「対策なし」モードで攻撃文字列を試すと、WHERE句が壊れて全ユーザーの情報が漏洩してしまう様子が確認できたはずです。一方「対策あり」モードでは、同じ文字列を入力してもSQL文の構造自体は?のまま変わらず、入力値は単なるデータとして扱われるため、該当なしという結果になります。これが「値として渡す」ことの意味です。
最も基本的かつ効果的な対策は、SQL文の組み立て時にプレースホルダ(パラメータ化クエリ)を使うことです。
SELECT * FROM users WHERE name = ? AND password = ?
このように、SQL文の「構造」と「値」を分離して送ります。データベースドライバは?の部分に入る値を、あくまで「データ」として扱い、SQL文の一部として解釈することはありません。どれだけクォートや--を含む文字列を入力しても、SQL文の構造自体は変化しないため、攻撃が成立しなくなります。
実務では、SQL文を直接書かずにORM(Object-Relational Mapping)を使うケースも多くあります。
Django ORMやSQLAlchemyといったORMは、内部的にプレースホルダを使ってクエリを発行するため、通常の使い方をしている限り、SQLインジェクションのリスクを大きく下げられます。
フレームワークやORMの選び方については、 DBを柔軟に設計できるWebアプリ開発 でも詳しく解説しています。
ただし、ORMを使っていても「生SQLを文字列連結で組み立てて実行する」機能を使ってしまうと、対策は無効になります。ORMを導入すること自体がゴールではなく、値を必ずパラメータとして渡すという原則を守ることが重要です。
最後の「権限を絞る」は、万が一プレースホルダの対策が漏れていた場合でも被害を最小化するための保険です。対策は1つに頼らず、重ねておくのが実務上の基本になります。
SQLインジェクションは、入力値をSQL文に文字列連結で埋め込むことで発生します。対策の核心はプレースホルダ(パラメータ化クエリ)を使い、値を必ずデータとして渡すことです。
ORMを使う場合でも、この原則を意識しているかどうかで安全性は大きく変わります。まずは自分のアプリのDB操作部分が、文字列連結になっていないかを確認するところから始めてみてください。