Rails: Active Recordのメソッドに渡す文字列を式展開してはいけない(翻訳)
悪意のあるユーザーからアプリケーションを保護するのは、開発者の重要な責務のひとつです。これは、Railsのように、セキュリティ機能が組み込まれているメンテの十分なフレームワークを使う優れた理由となります。
特に大きな理由は、ユーザー入力をデータベースに保存する前にサニタイズするActive Recordの保護機能です。ただし、その気になればActive Recordのスコープに文字列を直接渡す方法がいくつもあります。しかしこれは極めて慎重かつ控えめに使う必要のある、要注意の機能です。
🔗 以下のように書いてはいけない
Active Recordに渡す引数で文字列の式展開を使う。
User.delete_by("id = #{params[:id]}")
User.where("email = #{params[:email]}")
🔗 以下のように書くこと
ハッシュ形式の構文にする。
User.delete_by(id: params[:id])
User.where(email: params[:email])
🔗 そうする理由
Railsは切れ味の鋭い刃物です。開発者にとって有用なこともたくさんありますが、ユースケース(ここではActive Recordのメソッドに文字列を渡す)に合わせてフレームワークを柔軟に捻じ曲げることも可能になっています。
Active Recordには、データベースを素直に操作できるメソッドが豊富に揃っているので、上の例のような形で文字列をActive Recordに渡すことはめったにないと思いますが、SQLクエリが長く複雑になると、ユーザー入力のサニタイズを忘れがちになります。
文字列でパラメータを式展開すると、ユーザー入力からのSQLインジェクション攻撃にさらされる可能性が生じます。
# ユーザーが入力したパラメータ
params[:id] = "1) OR 1=1--"
User.delete_by("id = #{params[:id]}")
#=> User Delete All (4.2ms) DELETE FROM "users" WHERE (id = 1) OR 1=1--)
上のユーザー入力に含まれている1=1
の部分は常にtrueになるので、データベースの全ユーザーを削除するSQLクエリが発行されてしまいます。これは良くない事態です。
値を直接引数に式展開すると、上のような悪意に基づいた破滅的な結果以外にも、想定外の振る舞いが生じる可能性があります。たとえば、以下のように想定外の情報が漏洩することも考えられます。
params[:q] = "'' OR 1=1"
User.where("email = #{params[:q]}")
#=> User Load (1.1ms) SELECT "users".* FROM "users" WHERE (email = '' OR 1=1)
この例では、サニタイズされていない1=1
がSQLのWHERE
条件に渡されると常にtrueになってしまうので、データベースからすべてのユーザーを読み出すSQLクエリが発行され、情報が漏洩してしまいます。
最後に、引数を文字列ベースにするとコードが読みにくくなり、理解が難しくなります。ハッシュベースの構文を使う方がずっと理解しやすくなります。
🔗 そうしない理由があるとすれば
お見せした例はいかにも作為的ですが、ここには実際の弱点が示されています。
必要なクエリでハッシュスタイルの構文を利用できない事情がある場合は、自分がやっていることを十分に、徹底的に、確実に理解してから文字列ベースの引数を使いましょう。くれぐれも細心の注意を払ってください。
概要
原著者の許諾を得て翻訳・公開いたします。
参考: § 3.3 条件でハッシュを使う -- Active Record クエリインターフェイス - Railsガイド