📝

ActiveRecord SQLインジェクションクイズ (Rails 7.1.3.4)

2024/07/20に公開

問題

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️⃣

1️⃣ User.find_by(id: params[:id])

エスケープ処理が入るため これは安全な呼び出しです

Railsガイドにも引用があります。
https://railsguides.jp/security.html#sqlインジェクション-対策

Model.find(id)やModel.find_by_*(引数)といったクエリでは自動的にこの対策が適用されます。

2️⃣ User.where("id = #{params[:id]}")

SQLフラグメントはエスケープ処理されないためSQLインジェクション可能な危険な呼び出しです

Railsガイドでも代表的なダメな例としてあげられるパターンです。
https://railsguides.jp/security.html#sqlインジェクション-はじめに

3️⃣ User.order("id #{params[:order]}")

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の実装としては以下で、
https://github.com/rails/rails/blob/v7.1.3.4/activerecord/lib/active_record/relation/query_methods.rb#L1875-L1878

PostgreSQLの場合はこのパターンにマッチしないといけない(各アダプターでそれぞれのDBMSに対したパターンが定義されている)
https://github.com/rails/rails/blob/v7.1.3.4/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb#L177-L190

この例外が発生する振る舞いはRails 6.1からです(5.2からDEPRECATION WARNING)。古いバージョンのRailsにおいては問題があるコードというひっかけ問題でした。
https://github.com/rails/rails/blob/6-1-stable/activerecord/CHANGELOG.md#rails-610-december-09-2020
https://github.com/rails/rails/pull/27947

4️⃣ User.exists?(params[:id])

パラメータを工夫することでSQLフラグメントを渡せるパターンがありSQLインジェクションが可能なため危険な呼び出しです

まず引数パターンによる呼び出しの種類を確認します。
https://github.com/rails/rails/blob/v7.1.3.4/activerecord/lib/active_record/relation/finder_methods.rb#L321-L349

    #   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にも目を通すと理解が深まるでしょう。
https://rails-sqli.org/#exists

このクイズを通して考えたいこと

ユーザー入力を直接渡すな!という話はそれはそう。大前提としてそれはまず最初に守らなければならないこと。とはいえ実際の業務コードにおいては直接ではないにしてもユーザー入力から動的にSQLフラグメントを組み立てるケースが少なからずあり、何かしらの実装ミスが重なることでSQLインジェクション可能な脆弱コードが生まれる可能性がありえます。

ActiveRecordのクエリI/Fとその脆弱なパターンを漏れなく覚えていればそういったコードを書くことは抑止され、またコードレビューで指摘して防ぐ可能性も上がるでしょう。とはいえRails SQL Injection Examplesを見てみてもSQLインジェクション可能なコードというのが意外とあって全部完璧に覚えるのも正直ダルい...(これは個人の感想)
https://rails-sqli.org/

また3️⃣の例でいえば将来のバージョンで振る舞いが変わるものもあるよねっていう。

というわけで脆弱なコードというやつはツールやAIといったテクノロジーあたりで機械的に検知したいよねと考えていて、Brakemanというツールが最近気になっている。
https://brakemanscanner.org/

Railsに特化したセキュリティの静的解析ツールです。しばらくこれを弄って評価してみたいなということで、次回に続きます。

To be continued...

Discussion