SQLインジェクション
SQL Injection ・ えすきゅーえるいんじぇくしょん
入力がSQLの構文として読まれ、意図しないクエリを実行される脆弱性。対策の本命はプレースホルダ。
✦ 30秒で試す — 航海危ない文字を消すより確実な、たった1つの書き方O'Reilly さんのログインはどうなる?予想してみる概要
SQLインジェクションは、アプリが利用者の入力を使って SQL の文を組み立てるときに、入力の一部が「値」ではなく「命令(構文)」として読まれてしまい、開発者が意図しないクエリをデータベースに実行させてしまう脆弱性です。ログインフォームの認証の回避、他人のデータの読み出し、データの書き換えまで、被害はそのアプリがデータベースに対してできることすべてに及びます。
有名な例が、パスワード欄に ' OR '1'='1 という文字列を入れると、パスワードを知らなくてもログインが通ってしまう、というものです。入力の先頭の ' が、アプリが書いた「ここからここまでが値」という引用符を途中で閉じ、残りの OR '1'='1' が条件式の一部として読まれるためです。1990年代の終わりから知られている古い問題ですが、いまも大きな情報漏えいの原因になり続けています。
なぜ生まれたか
根にあるのは、SQL が「命令」と「値」を1本の文字列の中に混ぜて書く言語だという性質です。SELECT * FROM users WHERE name = 'alice' では、SELECT や WHERE は命令で、引用符に囲まれた alice だけが値です。どこまでが値かを決めているのは引用符という1文字の記号なので、アプリが入力をそのまま文字列として連結すると、入力の中の ' が値の終わりの印として働いてしまいます。
Web アプリが広まった1990年代後半、フォームの値を HTML から受け取り、そのまま WHERE 句にはめ込んでデータベースに送る、という素朴な書き方が一般的でした。1998年には Phrack 誌で、この書き方を突く手口が公に解説されています。これは XSS が「HTML の中でデータとコードの境界が崩れる」問題であるのと同じ構図で、どちらも「外部からの入力を、うっかりコードとして解釈させてしまう」ことが原因です。そこで対策も、危ない文字を探して消す方向から、「構文と値を別々の経路で渡し、混ざらないようにする」方向へ進みました。それがプレースホルダ(パラメータ化クエリ)です。
詳細
境界が壊れる瞬間
アプリが SELECT * FROM users WHERE name = '<名前>' AND password = '<パスワード>' という SQL を文字列の連結で作っているとします。正規の入力と ' OR '1'='1 を、同じ連結の処理に通して比べます。
攻撃の入力では、先頭の ' がアプリの書いた引用符と組になって空の値 '' を作り、値の箱がそこで閉じます。続く OR '1'='1 はもう値の中ではないので、データベースのパーサ(構文解析器)はこれを条件式の一部として読みます。最後の '1 は、アプリが末尾に書いた引用符と組になってちょうど閉じます。AND は OR より先に結び付くので、条件全体は (name = 'alice' AND password = '') OR '1'='1' になり、どの行でも真です。アプリは「1行でも返れば、その先頭の利用者としてログイン」という作りなので、パスワードの照合が無意味になります。
同じ構造は、認証の回避以外にも使われます。検索結果を画面に出す機能なら、値の箱を閉じたあとに UNION SELECT を続けることで、別の表(たとえば利用者の表)の列を検索結果に混ぜて表示させられます。画面にはアプリが意図した商品名の代わりに、別の表の中身が並ぶことになります。画面に結果が出ない場所でも、応答の違いや時間の差から少しずつ情報を推測される「ブラインド SQL インジェクション」があるので、「結果を表示していないから安全」とは言えません。
; で文を区切って DROP TABLE のような2つ目の文を続ける例もよく挙げられますが、多くのデータベースのドライバは、1回の呼び出しで複数の文を実行すること(複文)を既定で禁止しています。とはいえ複文を許す設定やドライバもあり、そもそも1つの文の中だけでも上のように読み出しや認証の回避はできるので、「複文が禁止だから大丈夫」ではありません。
対策の本命: プレースホルダ
本質的な対策は、プレースホルダ(パラメータ化クエリ、バインド変数とも)を使うことです。WHERE name = ? AND password = ? のように、値の位置を ? にした SQL を先にデータベースへ渡して構文を確定させ、値はあとから別の経路で渡します。データベースは、値が届く前に構文の解析を終えているので、値の中に ' や OR が含まれていても、それを構文として読み直すことはありません。' OR '1'='1 を渡しても、「パスワードが ' OR '1'='1 という文字列の利用者」を探すだけです。
プリペアドステートメントでは、この2段階がそのまま通信の往復になります。
大事なのは、この対策が「危ない文字を探して消す」のではなく、「構文と値の経路を最初から分ける」考え方だという点です。入力の ' を '' に置き換えるエスケープでも防げる場面はありますが、出力する場所ごとに正しい関数を漏れなく通す必要があり、1か所の書き忘れが穴になります。さらに、接続の文字コードの設定とエスケープ関数の想定が食い違い、マルチバイト文字の一部として引用符のエスケープが無効になった事例もありました。なお、ドライバによってはプレースホルダを内部で展開してから送る(エミュレーション)ものもありますが、その場合もエスケープはドライバが一か所で正しく行うので、アプリの書き方としてはプレースホルダを使うことに変わりはありません。接続の文字コードを明示しておくことだけは忘れないようにします。
下の部品で、同じ入力が文字列連結とプレースホルダでどう読まれるかを見比べてください。' OR '1'='1 のほかに、O'Reilly のようなごく普通の名前でも、連結では SQL が壊れることが分かります。
| id | name | password |
|---|---|---|
| 1 | alice | pass123 |
| 2 | bob | b0b-2024 |
| 3 | O'Reilly | tea4two |
下の ' OR '1'='1 をつかんでパスワード欄に落としてください(タップでも入ります)。組み立てられた SQL の上で、入力の文字がどこまで「値の箱」の中にいるかが色で分かります。欄に直接打っても構いません。
DB には接続していません。実際のパスワードはハッシュ化して保存・照合しますが、ここでは構文と値の境界の話に絞るため平文で表しています。
ORM と、プレースホルダが使えない場所
現代のアプリは ORM(オブジェクトと関係データベースの表を対応付けるライブラリ)を通してデータベースを扱うことが多く、ORM が発行するクエリは内部でプレースホルダを使うので、ふつうに使っていれば自動的に守られます。ただし、複雑な集計や動的な条件のために「生 SQL」を書く箇所は残りがちで、そこで文字列を連結すれば穴が開きます。ORM の多くには、生 SQL の断片を受け取る関数(raw や where に文字列を渡す書き方)があり、そこに入力を連結するのが典型的な失敗です。
もう一つの落とし穴は、プレースホルダで渡せない部分です。? に入れられるのは値だけで、表の名前・列の名前・ORDER BY の並び順・ASC と DESC のような構文の一部は入れられません。並び替えの列を利用者に選ばせる画面では、入力をそのまま連結するのではなく、「許可する列名の一覧」と照合して、一覧にあるものだけを使います。コードレビューや監査では、「生 SQL を書いている箇所」と「識別子を動的に組み立てている箇所」を洗い出すのが要点です。
多層防御
土台をプレースホルダで固めたうえで、万一の被害を小さくする備えを重ねます。アプリが使うデータベースのアカウントの権限を必要最小限にすれば(最小権限の原則。たとえば読み取りだけの画面には SELECT 権限しか与えない)、注入されても書き換えや削除はできません。エラーメッセージにデータベースの内部情報(SQL の断片や表の名前)を出さないことも、手がかりを与えない備えになります。WAF(Web Application Firewall)は既知のパターンを入り口で止められますが、書き方を変えた入力をすべて見分けることはできません。これらはあくまで補助で、根本対策はプレースホルダの徹底だという優先順位を取り違えないことが肝心です。SQL インジェクションは、同じ「入力がコードとして読まれる」構図を持つ XSS やコマンドインジェクションと並んで、ほとんどの脆弱性診断で最初に確かめられる項目です。
試問
ショップのログイン画面を、守る側の立場で直します。ユーザー名とパスワードの欄に入れた値でアプリが SQL(データベースへの命令文)を作り、下にデータベースに届く SQL と、利用者の表(O’Reilly さんのパスワードは tea4two)が表示されます。アプリの書き方は、入力を SQL の文字列に直接つなげる「文字列連結」と、構文と値を別々に送る「プレースホルダ」から選べます。入力のチップは欄にドラッグするか、タップで入ります。
| id | name | password |
|---|---|---|
| 1 | alice | pass123 |
| 2 | bob | b0b-2024 |
| 3 | O'Reilly | tea4two |
下の ' OR '1'='1 をつかんでパスワード欄に落としてください(タップでも入ります)。組み立てられた SQL の上で、入力の文字がどこまで「値の箱」の中にいるかが色で分かります。欄に直接打っても構いません。
DB には接続していません。実際のパスワードはハッシュ化して保存・照合しますが、ここでは構文と値の境界の話に絞るため平文で表しています。
