年末年始でセキュリティ分野を学習し、年明けにウェブ・セキュリティ基礎試験(通称:徳丸基礎試験)を受験することにしました。そこで勉強した内容を備忘録としてまとめます。今回はSQLインジェクションについてです。
記事の前半ではSQLインジェクションの概要や原因・一般的な対策などを、後半ではRailsにおけるSQLインジェクション対策をまとめます。
SQLインジェクションとは何なのか?
SQLインジェクションは、攻撃者が悪意のあるコードをSQLクエリに注入することで、データベースに予期しないコマンドを実行させるセキュリティ上の脆弱性です。不正なSQLクエリをデータベースに送信することで悪質なデータ操作ができてしまいます。
その結果、機密情報へのアクセスやデータの改ざんなど深刻なセキュリティ被害を引き起こします。さらにテーブル削除なども実行されうるため、最悪の場合サービスの存続ができなくなってしまう恐れすらあります。
SQLインジェクションはどこでどうやって発生するのか?
SQLインジェクションは、ユーザーから受け取った入力を元にSQLクエリを発行する箇所で発生する可能性があります。入力値を検証・エスケープせずにそのままSQLクエリへ組み込んでしまうことが原因だからです。エスケープとはプログラミング言語にとって特別な意味を持つ文字や記号を、別の文字に置き換えることです。
ログインフォームを例に考える
これだけじゃ分かりにくいのでログインフォームを想定し具体的なSQLクエリを考えてみます。たとえばemail
とpassword
の入力を受け取ってWHERE
句に指定し、ユーザーが存在すればログインできる機能があったとします。
# email と password をユーザーの入力から受け取る
SELECT * FROM users WHERE email = 'sample@gmail.com' AND password = 'password';
↑の例ではパスワード入力欄に「password」という文字列が入力されています。しかしここで、もしパスワード入力欄に' OR 'a'='a
という文字列が入力されたらどうなるでしょうか?
# パスワード入力欄に ' OR 'a'='a と入力されたら...
SELECT * FROM users WHERE email = 'sample@gmail.com' AND password = '' OR 'a'='a';
この場合、' OR 'a'='a
という入力値もSQLの一部として解釈されてしまいます。その結果、WHERE句のWHERE email = 'sample@gmail.com' AND password = '' OR 'a'='a'
は必ずtrue
になるため、パスワードを知らなくてもログインできてしまうのです。
SQLインジェクションが生まれてしまう背景
SQLインジェクションが生まれてしまう背景として、シングルクォートの存在があります。SQLの標準規格では文字列リテラルをシングルクオートで囲むルールなんですが、これを悪用し' OR 'a'='a
といった文字列リテラルを挿入することで、入力値もクエリの一部として解釈させることができてしまうのです。
SQLインジェクションはどうやったら防げるのか?
SQLインジェクションの防止策として以下の方法があります。
- プリペアードステートメント
- データベースのエラーメッセージをそのまま表示しない
- 入力値のバリデーションチェック
圧倒的に重要なのは1のプリペアードステートメントで、他の2つは保険的な対策に過ぎません。
①プリペアードステートメント
SQLインジェクションを防ぐ代表的な手法はプリペアードステートメントです。
たとえば冒頭のログインフォームのSQLだと、以下のようにプリペアードステートメントを定義できます。
# email と password は実際の処理時に受け取った値が代入される
# ? のことを「プレースホルダ」という
SELECT * FROM users WHERE email = ? AND password = ?;
↑の?
をプレースホルダと言います。プレースホルダには実際の処理時に受け取った値が代入されます。
このように入力値が格納される部分がパラメータ化されたSQLをあらかじめ作成しておくことで、クエリの構造を事前に確定させることができます。その結果、' OR 'a'='a
といった悪意のある入力値が後から渡されたとしても、クエリの一部としては解釈されずに済むのです。
さらにプレースホルダに格納される値はユーザーからの入力を受け取るタイミングでデータベース側が適切にエスケープしてくれるため、シングルクオート(''
)やセミコロン(;
)などSQLで用いられる特殊文字は安全に扱われます。つまりプリペアードステートメントによってSQLインジェクションはほぼ完全に防げるのです。
②データベースのエラーメッセージをそのままユーザーに表示しない
大前提としてプリペアードステートメントの活用を徹底することがSQLインジェクションの最大の対策なので、ここからはそれ以外の保険的な対策です。
まずはデータベース側のエラーメッセージをそのままユーザーに表示しないことが大切です。そのエラーメッセージがシステムの内部情報を漏らし、攻撃者にヒントを与えてしまう可能性があるからです。
エラーメッセージにはデータベースの構造やテーブル・カラム名、データベースの種類やバージョンなどが含まれることがあります。攻撃者はこの情報を手がかりに、より効果的な攻撃やクエリの調整をできてしまうのです。
そのため詳細なエラー情報はログファイルにのみ記録しユーザーには「エラーが発生しました」等の一般的なメッセージを表示することで、攻撃を受ける可能性を少しばかり軽減できます。
ただし根本的な対策にはならないので、やはりプリペアードステートメントの活用が最も効果的です。
③入力値のバリデーションチェック
入力値のバリデーションチェックも多少はSQLインジェクションの対策となりえます。
郵便番号なら数値のみ、パスワードなら半角英数字のみなど、サービスの要件に沿ったバリデーションを適用していれば、SQLの特殊文字の混入を未然に防げるため、仮にプレースホルダの利用が漏れていても攻撃は成立しません。
ただしメッセージ投稿やコメント欄など、自由入力の箇所も存在するため、バリデーションチェックだけでは完全には防げません。なのでプレースホルダの活用が欠かせないかなと思います。
RailsにおけるSQLインジェクション対策
ここからはRailsにおけるSQLインジェクション対策についてまとめます。まずはOKな書き方から紹介します。
OKな書き方
Railsにはデフォルトでシングルクォート(''
)やダブルクォート(""
)などSQLの特殊文字をエスケープする仕組みが備わっています。そのため以下のように書けばSQLインジェクションの心配は必要ありません。
email = params[:email]
password = params[:password]
# OK例
User.where(email: email, password: password)
直接SQLを書きたい場合、以下のようにプレースホルダを使い、第2引数以降に入力値をパラメータとして渡せばOKです。
email = params[:email]
password = params[:password]
# プレースホルダを使ってもOK
User.where("email = ? AND password = ?", name, password)
where
メソッド以外、たとえばfind
メソッドやfind_by
メソッドでも自動的にエスケープされます。
# 問題なし
user_id = params[:id]
User.find(user_id)
# 問題なし
email = params[:email]
User.find_by(email: email)
NGな書き方
RailsでSQLインジェクションが特に問題になるのは、ユーザーからの入力を直接SQLクエリの文字列に組み込んだ場合です。この方法は、ユーザーからの入力がクエリの一部として解釈される可能性があります。そのため悪意のある入力によってSQLクエリが意図しない形で実行されるリスクを高めます。
具体的には、シングルクォートなどの特殊文字がクエリの構造を変えるために使われる可能性があり、これによってデータベースが不正に操作される恐れがあります。以下はその一例です。
email = params[:email] # sample@gmail.com
password = params[:password] # ' OR 'a'='a
# パラメータをそのままSQLに組み込むのはNG
# ユーザの入力を直接SQLに組み込むと特殊文字がクエリの構造を変えてしまう恐れがある
# たとえばpasswordに ' OR 'a'='a が渡されると、WHERE句が常に真となってしまう
User.where("email='#{email}' and password='#{password}'")
#=> SELECT `users`.* FROM `users` WHERE `users`.`email` = 'sample@gmail.com' AND `users`.`password` = '\' OR \'a\'=\'a'
この書き方をしてしまうと、冒頭のログインフォームのようにpasswordとして渡された文字列の' OR 'a'='a
がSQLクエリの一部として扱われるため、WHERE句が必ずtrue
となってしまいます。
まとめ
SQLインジェクションの概要とRailsにおける対策方法について理解できました。
コメント