【Rails】SQLインジェクションとRailsにおける対策まとめ

年末年始でセキュリティ分野を学習し、年明けにウェブ・セキュリティ基礎試験(通称:徳丸基礎試験)を受験することにしました。そこで勉強した内容を備忘録としてまとめます。今回はSQLインジェクションについてです。

記事の前半ではSQLインジェクションの概要や原因・一般的な対策などを、後半ではRailsにおけるSQLインジェクション対策をまとめます。

目次

SQLインジェクションとは何なのか?

SQLインジェクションは、攻撃者が悪意のあるコードをSQLクエリに注入することで、データベースに予期しないコマンドを実行させるセキュリティ上の脆弱性です。不正なSQLクエリをデータベースに送信することで悪質なデータ操作ができてしまいます。

その結果、機密情報へのアクセスやデータの改ざんなど深刻なセキュリティ被害を引き起こします。さらにテーブル削除なども実行されうるため、最悪の場合サービスの存続ができなくなってしまう恐れすらあります。

SQLインジェクションはどこでどうやって発生するのか?

SQLインジェクションは、ユーザーから受け取った入力を元にSQLクエリを発行する箇所で発生する可能性があります。入力値を検証・エスケープせずにそのままSQLクエリへ組み込んでしまうことが原因だからです。エスケープとはプログラミング言語にとって特別な意味を持つ文字や記号を、別の文字に置き換えることです。

ログインフォームを例に考える

これだけじゃ分かりにくいのでログインフォームを想定し具体的なSQLクエリを考えてみます。たとえばemailpasswordの入力を受け取って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. データベースのエラーメッセージをそのまま表示しない
  3. 入力値のバリデーションチェック

圧倒的に重要なのは1のプリペアードステートメントで、他の2つは保険的な対策に過ぎません。

①プリペアードステートメント

SQLインジェクションを防ぐ代表的な手法はプリペアードステートメントです。

プリペアードステートメントとは?

プリペアードステートメントとは、あらかじめユーザーからの入力以外の部分のSQLを作成しておき、入力値を後からSQLにパラメータとして渡す手法です。

たとえば冒頭のログインフォームのSQLだと、以下のようにプリペアードステートメントを定義できます。

# email と password は実際の処理時に受け取った値が代入される
# ? のことを「プレースホルダ」という
SELECT * FROM users WHERE email = ? AND password = ?;

↑の?をプレースホルダと言います。プレースホルダには実際の処理時に受け取った値が代入されます。

このように入力値が格納される部分がパラメータ化されたSQLをあらかじめ作成しておくことで、クエリの構造を事前に確定させることができます。その結果、' OR 'a'='aといった悪意のある入力値が後から渡されたとしても、クエリの一部としては解釈されずに済むのです。

さらにプレースホルダに格納される値はユーザーからの入力を受け取るタイミングでデータベース側が適切にエスケープしてくれるため、シングルクオート('')やセミコロン(;)などSQLで用いられる特殊文字は安全に扱われます。つまりプリペアードステートメントによって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における対策方法について理解できました。

参考文献
よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

未経験でSESから従業員300名以上の自社開発企業に転職しました。業務や個人開発で直面した問題や、転職・学習の経験を発信していきます。

コメント

コメントする

目次