ホームページでユーザーの入力によってデータベースに関わる機会がよくあると思います。例えば、キーワードを入力して何かを検索する際などです。

悪意を持つユーザーがSQL文などを入れることで、サービスに不正な操作をさせることができます。これはSQLインジェクションと呼ばれます。このような悪質なユーザーに対して、SQLインジェクションへの対策を用意しなければなりません。ユーザーが入力した文字列をエスケープする方法が一番使われていると思います。

ある仮のRailsアプリに、Userというモデルがあり、レコードが5個あるとします。

User.all

// 結果
User id: 1, name: "Rooter", age: 10, gender: "male"
User id: 2, name: "Ruby", age: 15, gender: "female"
User id: 3, name: "Rails", age: 18, gender: "male"
User id: 4, name: "Sakura", age: 18, gender: "female"

このアプリには、ホームページに検索用のキーワード入力フィールドが存在しているとします。入力された文字列をアプリ側から変数に渡し、クエリストリングに入れ、Userからレコードを検索します。以下では、SQLインジェクションを防ぐための書き方と防がない書き方を紹介します。

whereのエスケープ

whereは以下のように、様々な書き方があります。

target_gender = "female"

User.where("gender = '#{target_gender}'")
// または
User.where("gender = ?", target_gender)
// または
User.where(gender: target_gender)
// SELECT  "users".* FROM "users" WHERE (gender = 'female')

// 結果
User id: 2, name: "Ruby", age: 15, gender: "female"
User id: 4, name: "Sakura", age: 18, gender: "female"

例えば、ユーザーが入力した文字列を変数に渡し、その変数を検索条件としてwhereクエリに入れるという形のプログラムがあります。もしユーザーが間違えて、または悪意を持ちながら変な文字列を入力した場合、適当な処理をせずにそのまま文字列をクエリに入れると(例えば下のような書き方)、望ましくない検索結果が出てしまう可能性があり、アプリには危険です。

target_gender = "male' OR name LIKE '%ra%"
User.where("gender = '#{target_gender}'")
// SELECT  "users".* FROM "users" WHERE (gender = 'male' OR name LIKE '%ra%')

// 結果
User id: 1, name: "Rooter", age: 10, gender: "male"
User id: 3, name: "Rails", age: 18, gender: "male"
User id: 4, name: "Sakura", age: 18, gender: "female"

だが実際には、Activerecordのwhereは強いメソッドです。以下のような、ハッシュで渡す書き方、または疑問符を使う書き方にすれば、エスケープしてくれます。

target_gender = "male' OR name LIKE '%ra%"
User.where(gender: target_gender)
// SELECT  "users".* FROM "users" WHERE "users"."gender" = ?  [["gender", "male' OR name LIKE '%ra%"]]
// または
User.where("gender = ?", target_gender)
// SELECT  "users".* FROM "users" WHERE (gender = 'male'' OR name LIKE ''%ra%')

// 結果
[]

ハッシュで渡す書き方が適用されない場合にも、できれば疑問符の書き方を使ってください

target_name = 'Sakura'
target_age = 12
// User.where("name = #{target_name } or age < #{target_age}") を使わないで
User.where("name = ? or age < ?", target_name, target_age)
// SELECT  "users".* FROM "users" WHERE (name = 'Sakura' or age < 12)

// 結果
User id: 1, name: "Rooter", age: 10, gender: "male"
User id: 4, name: "Sakura", age: 18, gender: "female"

sanitize_sql_* メソッドでエスケープ

whereのみならず、他のところにもsqlクエリストリングを使いたいがSQLインジェクションを避けなければならない場合は、ActiverecordのSanitizationモジュールのクラスメソッド:sanitize_sql_array, sanitize_sqlなどの出番です。

検索結果を邪魔する変数をsanitize_sql_arrayにより、クエリストリングに入れた際にエスケープされます。以下の例をご覧ください。

target_gender = "male' OR name LIKE '%ra%"
search_sql = User.sanitize_sql_array(["gender = ?", target_gender])

User.where(search_sql)
// SELECT  "users".* FROM "users" WHERE (gender = 'male'' OR name LIKE ''%ra%')

// 結果
User id: 3, name: "Rails", age: 18, gender: "male"
User id: 4, name: "Sakura", age: 18, gender: "female"

複数の変数でも対応できます。

target_gender = "male' OR name LIKE '%ra%"
search_sql = User.sanitize_sql_array(["gender = ? or age > ?", target_gender, 15])
// => "gender = 'male'' OR name LIKE ''%ra%' or age > 15"
User.where(search_sql)
// SELECT  "users".* FROM "users" WHERE (gender = 'male'' OR name LIKE ''%ra%' or age > 15)

// 結果
User id: 3, name: "Rails", age: 18, gender: "male"
User id: 4, name: "Sakura", age: 18, gender: "female"

ただ、Rails5.2より前はsanitize_sql_arrayメソッドがprivateメソッドだったので、モデル以外で使うには User.send(:sanitize_sql_array, ["gender = ? or age > ?", target_gender, 15]) で書かなければいけませんでしたが、Rails5.2以降は普通に書いても大丈夫です。

以下はsanitize_sql_arrayの書き方一覧とそれぞれでエスケープして出力されるクエリストリングです。

User.sanitize_sql_array(["name = ? and age > ?", "ruby'on'rails", 15])
=> "name = 'ruby''on''rails' and age > 15"

User.sanitize_sql_array(["name = :name and age > :age", name: "ruby'on'rails", age: 15])
=> "name = 'ruby''on''rails' and age > 15"

User.sanitize_sql_array(["name = '%s' and age > %d", "ruby'on'rails", 15])
=> "name = 'ruby''on''rails' and age > 15"

もう一つ簡単に紹介します。sanitize_sql(またはsanitize_sql_for_conditions)は、sanitize_sql_arrayと同じ機能ですが、sanitize_sql_arrayは配列しか受け入れず、sanitize_sqlは文字列でも一応受け入れます(ただその文字列は既にエスケープされている必要があります)。以下はsanitize_sqlの書き方一覧とそれぞれで出力されるクエリストリングです。

User.sanitize_sql(["name = ? and age > ?", "ruby'on'rails", 15])
=> "name = 'ruby''on''rails' and age > 15"

User.sanitize_sql(["name = :name and age > :age", name: "ruby'on'rails", age: 15])
=> "name = 'ruby''on''rails' and age > 15"

User.sanitize_sql(["name = '%s' and age > %d", "ruby'on'rails", 15])
=> "name = 'ruby''on''rails' and age > 15"

User.sanitize_sql("name = 'ruby''on''rails' and age > 15")
=> "name = 'ruby''on''rails' and age > 15"

終わりに

アプリのセキュリティのため、SQLインジェクションなどのリスクを防ぐのは重要な課題です。

参考

https://api.rubyonrails.org/classes/ActiveRecord/Sanitization/ClassMethods.html https://www.sejuku.net/blog/13363