RailsでのパラメータバインディングによるSQLインジェクション対応
RailsでのパラメータバインディングによるSQLインジェクション対応について記載する。
最初に、RailsガイドにSQLインジェクションの項があるため参照すること。
SQLインジェクションとは、攻撃者が不正なSQL文(データベースへの命令文)を注入(Inject)し、データベースを不正に操作する攻撃手法。
検証
Devise gemをインストールする際に作成されるusernameカラムを例にSQLインジェクションを検証する。
ORの例
params = {}
params[:username] = "' OR true) --";
User.where("username = '#{params[:username]}'")
# => 全件出力
' OR true) --という文字列を含んだ検索を実行すると、以下のSQLクエリが生成される。
SELECT * FROM "users" WHERE (username = '' OR true) --')
クエリ結果は、すべての条件がtrueとなっているためUserテーブル全件が返却される。
ここで通常のユーザーでは参照しないレコードを含めて出力される問題が発生する。
ANDの例
params = {}
params[:username] = "test-user' AND 1=1) --"
User.where("username = '#{params[:username]}'")
# => 'test-user'を含むレコードを出力
test-user' AND 1=1) --という文字列を含んだ検索を実行すると、以下のSQLクエリが生成される。
SELECT * FROM "users" WHERE (username = 'test-user' AND 1=1) --')
クエリ結果は、test-userというユーザーが存在すればそのUserテーブルが返却される。
test-userのみを検索したい場合であっても、外部からの入力がSQLの一部として解釈・実行されているためSQLインジェクション脆弱性が存在する。
対策 - プレイスホルダーを利用する
User.where("username = '#{params[:username]}'")のように文字列展開処理によってSQL文を組み立てるのではなく、プレイスホルダーを利用してSQL文を組み立てるようにする。プレースホルダに実際の値を割り当てる処理をパラメータバインドと呼ぶ。
where句に対して、パラメータバインディングを利用するように変更する。
ORの対策
params = {}
params[:username] = "' OR true) --";
User.where("username = ?", params[:username])
# => 0件出力
上記コードを実行すると、以下のSQLクエリが生成される。'文字列がサニタイズされて、SQL命令ではなく検索文字列となる。
SELECT * FROM "users" WHERE (username = ''' OR true) --')
クエリは、「username カラムの値が ''' OR true) --' という文字列と完全に一致するユーザー」となり、外部からの入力が解釈されないようになっている。
ANDの対策
params = {}
params[:username] = "test-user' AND 1=1) --";
User.where("username = ?", params[:username])
# => 0件出力
同様に、'文字列がサニタイズされて、SQL命令ではなく検索文字列となる。
SELECT * FROM "users" WHERE (username = 'test-user'' AND 1=1) --')
クエリは、「username カラムの値が 'test-user'' AND 1=1) --' という文字列と完全に一致するユーザー」となり、外部からの入力が解釈されないようになっている。
ラップアップ
プレースホルダを使うことで、入力された文字列全体がSQL命令ではなく単なる検索文字列として扱われるようになり、その結果、悪意のある文字列(例: 'test-user'' AND 1=1) --')に完全に一致するユーザー名は通常存在しないため、検索結果が0件になる。
文字列連結を利用すれば依然としてSQLインジェクションの可能性はあるため、SQLで外部入力文字列を使うときはサニタイズし、ユーザからの入力をSQL命令として扱わないこと。
参考
Discussion