ActiveRecord SQLインジェクションクイズ (Rails 7.1.3.4)
問題
ActiveRecordクイズです。SQLインジェクション可能なのはどれでしょう?
(Railsバージョンは7.1.3.4、複数回答あり)
- 1️⃣
User.find_by(id: params[:id])
- 2️⃣
User.where("id = #{params[:id]}")
- 3️⃣
User.order("id #{params[:order]}")
- 4️⃣
User.exists?(params[:id])
正解と解説
SQLインジェクション可能なのは 2️⃣ と 4️⃣
User.find_by(id: params[:id])
1️⃣ エスケープ処理が入るため これは安全な呼び出しです。
Railsガイドにも引用があります。
Model.find(id)やModel.find_by_*(引数)といったクエリでは自動的にこの対策が適用されます。
User.where("id = #{params[:id]}")
2️⃣ SQLフラグメントはエスケープ処理されないためSQLインジェクション可能な危険な呼び出しです。
Railsガイドでも代表的なダメな例としてあげられるパターンです。
User.order("id #{params[:order]}")
3️⃣ SQLフラグメントですがRails 7.1.3.4では内部でパターンチェックがあり、order句として不正な文字列の場合は例外が発生します。なのでSQLインジェクションに対しては危険ではありません。
irb(main):001> User.order('id asc; select * from users')
(irb):1:in `<main>': Dangerous query method (method whose arguments are used as raw SQL) called with non-attribute argument(s): "id asc; select * from users".This method should not be called with user-provided values, such as request parameters or model attributes. Known-safe values can be passed by wrapping them in Arel.sql(). (ActiveRecord::UnknownAttributeReference)
ActiveRecordの実装としては以下で、
PostgreSQLの場合はこのパターンにマッチしないといけない(各アダプターでそれぞれのDBMSに対したパターンが定義されている)
この例外が発生する振る舞いはRails 6.1からです(5.2からDEPRECATION WARNING)。古いバージョンのRailsにおいては問題があるコードというひっかけ問題でした。
User.exists?(params[:id])
4️⃣ パラメータを工夫することでSQLフラグメントを渡せるパターンがありSQLインジェクションが可能なため危険な呼び出しです。
まず引数パターンによる呼び出しの種類を確認します。
# Person.exists?(5)
# Person.exists?('5')
# Person.exists?(['name LIKE ?', "%#{query}%"])
# Person.exists?(id: [1, 4, 8])
# Person.exists?(name: 'David')
# Person.exists?(false)
# Person.exists?
# Person.where(name: 'Spartacus', rating: 4).exists?
IntegerおよびStringを一つだけ渡すのはプライマリーキーでクエリを投げます。これはエスケープされます。
問題はArrayを渡すパターンでプレースホルダーで値を割り当てるもの。この最初の要素はSQLとしてのエスケープはされない( ?
に割り当てられる値はエスケープされる)
なのでRailsにおいては以下のようなGETパラメータを与えることで、
?id[]=1
params[:id]
はArray型となり、任意のSQLフラグメントが指定可能になります。
Rails SQL Injectionにも目を通すと理解が深まるでしょう。
このクイズを通して考えたいこと
ユーザー入力を直接渡すな!という話はそれはそう。大前提としてそれはまず最初に守らなければならないこと。とはいえ実際の業務コードにおいては直接ではないにしてもユーザー入力から動的にSQLフラグメントを組み立てるケースが少なからずあり、何かしらの実装ミスが重なることでSQLインジェクション可能な脆弱コードが生まれる可能性がありえます。
ActiveRecordのクエリI/Fとその脆弱なパターンを漏れなく覚えていればそういったコードを書くことは抑止され、またコードレビューで指摘して防ぐ可能性も上がるでしょう。とはいえRails SQL Injection Examplesを見てみてもSQLインジェクション可能なコードというのが意外とあって全部完璧に覚えるのも正直ダルい...(これは個人の感想)
また3️⃣の例でいえば将来のバージョンで振る舞いが変わるものもあるよねっていう。
というわけで脆弱なコードというやつはツールやAIといったテクノロジーあたりで機械的に検知したいよねと考えていて、Brakemanというツールが最近気になっている。
Railsに特化したセキュリティの静的解析ツールです。しばらくこれを弄って評価してみたいなということで、次回に続きます。
To be continued...
Discussion