Rails ActiveRecord SQLインジェクション
どこにでも見かけるダメな例
Project.where("name = '#{params[:name]}'")
Ruby on Railsには、特殊なSQL文字をフィルタするしくみが組み込まれており、「'」「"」「NULL」「改行」をエスケープします。Model.find(id)やModel.find_by_*(引数)といったクエリでは自動的にこの対策が適用されます。ただし、SQLフラグメント、特に条件フラグメント(where("..."))、connection.execute()またはModel.find_by_sql()メソッドについては手動でエスケープする必要があります。
位置指定ハンドラ使え
Model.where("zip_code = ? AND quantity >= ?", entered_zip_code, entered_quantity).first
SQLを発行するためのI/Fを分類してみる
-
- SQLを隠蔽しているI/F e.g.
User.where(name: "Joe")
- SQLを隠蔽しているI/F e.g.
-
- SQLフラグメントを指定するI/F
- 2-1. プレースホルダーで値を割り当てる e.g.
User.where("name = ?", "Joe")
- 2-2. SQLフラグメントそのまま e.g.
User.where("name = 'Joe'")
1.
と 2-1.
はエスケープ処理が確実に行われる。SQLインジェクションに対して安全。
2-2.
はSQLインジェクションに対して脆弱。
User.where("name = ?", "Joe")
は静的プレースホルダなのか、動的プレースホルダなのか。おそらく後者であろうが、後で実装を確認する。
SQLインジェクションに対する根本的解決
SQL 文の組み立ては全てプレースホルダで実装する。
1.
と 2-1.
を利用する限りは満たせる。
SQL 文の組み立てを文字列連結により行う場合は、エスケープ処理等を行うデータ
ベースエンジンの API を用いて、SQL 文のリテラルを正しく構成する。
1.
と 2-1.
を利用する限りは満たせる。
ウェブアプリケーションに渡されるパラメータに SQL 文を直接指定しない。
それはそう。
#where
のように、
プレースホルダでSQLインジェクション対策ができるものはガイドにしっかりと言及がある。その通りに実装すればいい。
プレースホルダで解決するI/Fを持たない #order
や #join
などについて、SQLインジェクションがどのように対応されているのかは後ほど見ていく。
whereのサニタイズはここから
Quoting
各データベースアダプターのquote_xxxメソッドが呼ばれる
mysql2_adapterの場合
ここに行き着いた
postgresqlは #quote
からoverrideされていた
overrideしていると見せかけて、Stringの場合はsuperに移譲
mysqlと同じく、ドライバのメソッド呼び出している
ここに行き着いた
SQL_STR_DOUBLEのmacro
order by の処理を追いかける。
それぞれの column_name_with_order_matcher は以下の通り。
order句としておかしい文字列を突っ込まれたら、ここで弾かれる
https://rails-sqli.org/ を眺めて、SQLインジェクションのパターンを見る。SQLフラグメントを渡しているようには見えないけどSQLにそのまま文字列連結されるやつがいくつかある。
Calculate Methods
calculate(operation, column_name)
のcolumn_nameはエスケープなどされずにSQLに文字列連結される。
Exists? Method
引数に文字列のみを渡す場合はエスケープされる。配列もしくはHashの場合はconditions optionとして扱われる(のでエスケープされない)
From Method
引数の値はエスケープなどされずにSQLに文字列連結される。
Group Method
引数の値はエスケープなどされずにSQLに文字列連結される。
exists?
の引数バリエーションが多い
以下のようにしている場合、
User.exists? params[:user]
以下のGETパラメータを指定されると、
?user[]=1
第1引数はエスケープされないこの形式になる、という罠があるのか。
Person.exists?(['name LIKE ?', "%#{query}%"])
SQLインジェクションなどの脆弱性を静的解析するgem
Ruby Security お得情報
試しにこんなコードを書いてみて、
sql = if params[:flag] == 'on'
"name = '#{params[:name]}'"
else
"1 = 1"
end
Article.where(sql)
brakeman実行
Confidence: High
Category: SQL Injection
Check: SQL
Message: Possible SQL injection
Code: Article.where(("name = '#{params[:name]}'" or "1 = 1"))
File: app/controllers/articles_controller.rb
Line: 10
ちゃんとif文のtrue/falseのパス両方をチェックしてくれている。おもしろ!