ActiveRecordのSQLインジェクションパターン

(Last Updated On: 2018年10月7日)

Railsで多用されているActiveRecordのインジェクションパターンを簡単に紹介します。出典はrails-sqli.orgなのでより詳しい解説はこちらで確認してください。特に気をつける必要があると思われる物のみをピックアップしました。

Exists?メソッド

User.exists? params[:user]

params[:user]などの使い方は危険です。RailsはPHPなどと同様にuser[]というパラメーターで配列化します。

?user[]=1

が入力の場合、

SELECT 1 AS one FROM "users" WHERE (1) LIMIT 1

となり不正なクエリが実行されます。

Calculateメソッド

CalculateメソッドはSQLの集約関数を実行するメソッドです。average、calculate、count、maximum、minimum、sumがサポートされています。集約関数はコラム名を指定して実行します。コラム名はクエリパラメーターではなく識別子なのでプレイスホルダーなどはありません。

params[:column] = "age) FROM users WHERE name = 'Bob';"
Order.calculate(:sum, params[:column])

上記のような入力とコードでインジェクションが可能となります。

SELECT SUM(age) FROM users WHERE name = 'Bob';) AS sum_id FROM "orders"

この例では’Bob’の年齢が不正取得されていますが、アクセス権限のある物であればどれでも構いません。

自動エスケープすれば?と考える方もいると思いますが、自動エスケープでは、クエリ結果を集約できないなど、自由度を制限してしまいます。

Groupオプション

GROUP BYクエリを行うオプションです。GROUP BYはコラム名でグループ化するのですが、コラム名はパラメーターでなく識別子なのでプレイスホルダーはありません。コラム名をパラメーターとする場合、エスケープが必要です。

params[:group] = "name UNION SELECT * FROM users"
User.find(:all, :group => params[:group], :conditions => { :admin => false })

UNIONクエリが実行され、不正なレコードを取得されます。

Joinsメソッド

テーブルのJOINを行うメソッドです。JOINにはテーブル名を指定します。テーブル名はパラメーターではなく識別子なのでプレイスホルダーはありません。テーブル名をパラメーターとする場合、エスケープが必要です。

params[:table] = "--"
Order.where(:user_id => 1).joins(params[:table])

SQLのコメント文字”–“でJOINが無効化された結果が返されます。

Lockメソッドとオプション

Lockメソッドとオプションを使う場合、ユーザー入力を利用すると不正なクエリが実行できます。SQLiteはLockをサポートしていないので実際の攻撃例ではない、と注釈がありますが以下の例が紹介されています。

params[:lock] = "?"
User.where(1).lock(params[:lock])

この例では

 SELECT "users".* FROM "users" WHERE (1)

が実行され全てのユーザーレコードを取得しています。

Orderメソッド

ORDER BYクエリを実行するメソッドです。ORDER BYはコラム名でソート対象を指定します。このためプレイスホルダーなどは使えません。

params[:sortby] = "(CASE SUBSTR(password, 1, 1) WHEN 's' THEN 0 else 1 END)"
User.order("#{params[:sortby]} ASC")

この例ではUser情報を盗み出しています。少し分かりづらいのでクエリと実行結果も貼り付けます。

 Query
SELECT "users".* FROM "users" ORDER BY (CASE SUBSTR(password, 1, 1) WHEN 's' THEN 0 else 1 END) ASC
Result
[#<User id: 1692, name: "Admin", password: "supersecretpass", age: 21, admin: true, created_at: "2013-02-11 17:03:47", updated_at: "2013-02-11 17:03:47">, #<User id: 1687, name: "Bob", password: "Bobpass", age: 77, admin: false, created_at: "2013-02-11 17:03:47", updated_at: "2013-02-11 17:03:47">, #<User id: 1688, name: "Jim", password: "Jimpass", age: 18, admin: false, created_at: "2013-02-11 17:03:47", updated_at: "2013-02-11 17:03:47">, #<User id: 1689, name: "Sarah", password: "Sarahpass", age: 46, admin: false, created_at: "2013-02-11 17:03:47", updated_at: "2013-02-11 17:03:47">, #<User id: 1690, name: "Tina", password: "Tinapass", age: 73, admin: false, created_at: "2013-02-11 17:03:47", updated_at: "2013-02-11 17:03:47">, #<User id: 1691, name: "Tony", password: "Tonypass", age: 75, admin: false, created_at: "2013-02-11 17:03:47", updated_at: "2013-02-11 17:03:47">]

Orderメソッドの他にReorderメソッドの例としてSQL語句埋め込みによるSQLインジェクション例も紹介されています。

params[:order] = ", 8"
User.order("name DESC").reorder("id #{params[:order]}")

ActiveRecordに限らない、典型的なSQLインジェクションパターンです。SQL語句なのでプレイスホルダー、パラメーターエスケープ、識別子エスケープも使えないパターンであり、バリデーションするしかありません。この場合、”DESC”か”ASC”であることを確認するだけです。

Pluckメソッド

Pluckは特定のコラムを抽出するメソッドです。速いので便利です。他のメソッドと同様にコラム名がパラメーターの場合、危険です。

params[:column] = "password FROM users--"
Order.pluck(params[:column])

ユーザー入力を使うと任意のコラムの情報を不正取得される可能性があります。

Selectオプション

SELECTで抽出するコラムを指定するオプションです。他のメソッドと同様にコラム名がパラメーターの場合、危険です。

 params[:column] = "* FROM users WHERE admin = 't' ;"
User.first(:conditions => { :name => params[:name], :password => params[:password] }, :select => params[:column])

update_allのorderオプション

このorderオプションもカラム名を指定するオプおションです。この場合、()で囲まれているので OR 1=1という典型的なSQLインジェクションで全件取得が可能になります。

params[:order] = "name) OR 1=1;"
User.update_all("admin = 1", "name LIKE 'B%'" , { :order => params[:order] })

その他の典型的SQLインジェクション例

上記以外にもパラメーター埋め込みによる典型的なSQLインジェクション例が複数紹介されています。やはり埋め込んでしまうケースが後を絶たないのだと思われます。私がソースコード検査した物でも結構あります。

 params[:total] = "1 UNION SELECT * FROM orders"
Order.all(:conditions => { :user_id => 1 }, :group => :user_id, :having => "total > #{params[:total]}")

HAVINGはクエリ条件なのでパラメーターを埋め込んでしまうとインジェクションに脆弱になります。これらのクエリ条件にパラメーターを利用するケースは、WHEREと全く同じだと考えなければなりません。

紹介したSQLインジェクションに対する対策

まずは識別子(テーブル名とコラム名)のエスケープです。検索すると直ぐに出てきます。

http://apidock.com/rails/ActiveRecord/ConnectionAdapters/Quoting/quote_column_name
http://apidock.com/rails/v4.0.2/ActiveRecord/ConnectionAdapters/Quoting/quote_table_name

次に入力パラメーターのバリデーションです。ここで紹介したようなSQLインジェクションは普通に入力バリデーションを行っていればほとんど防御できます。しかし、Railsデフォルトの入力バリデーションの場合、モデルのフィールド名に対するバリデーションのみを考慮しているのでひと工夫が必要です。SQL語句、識別子にはバリデーションが効果的です。exists?メソッドのケースもバリデーションでも防御できます。私はStrong Parametersを使う時にコントローラーでバリデーションすると良いのでは?と思っています。他に良い方法がある場合は是非コメントをお願いします。

識別子はクエリ時にエスケープできるので、HTMLなどの出力先と同様にデフォルトでエスケープするとより安全になります。バリデーションしていても無条件でエスケープすることをお勧めします。

最後にクエリ条件となるパラメーターはプレイスホルダーを使うか、エスケープが必要です。どちらかを必ず使うようにします。

まとめ

個人的にはデータベース構造を操作するプログラム以外ではあまりコラム名やテーブル名をパラメーターとして受け取り利用することはありません。しかし、ソースコード検査を行っていると処理をまとめる為などの目的でデータベース構造を操作しないアプリであっても、コラム名やテーブル名に変数が利用されているケースは良く見かけます。

識別子のエスケープが必要であることは自明です。当たり前の事は当たり前に教え、当たり前に実行する必要があります。

参考

Railsのリモートコード実行脆弱性、今昔

投稿者: yohgaki

コメントは受け付けていません。