Ruby on Rails

【Rails】LIKE句とWHERE句のサニタイズについて(sanitize_sql_likeなど)

昨日ははじめてオフィスで仕事をしました。やはり見られる目があると集中できますね。

今回はRailsでSQLを扱う上で大切なサニタイズについて見ていこうと思います。大きく分けてLIKE句の中身のサニタイズとWHERE句の条件のサニタイズです。特にWHERE句の方では、脆弱性を生んでしまう危険性があるのでRailsエンジニアは知っておくべきでしょう。

WHERE句のエスケープ

検索機能を実装する際に以下のように書いてしまうとSQLインジェクションが発生する恐れがあります。
※ SQLインジェクションの詳細はこちらを御覧ください。

def search
  keyword = search_params[:search]
  @tasks = Task.where("tasks.category = %#{keyword}%")
end

このように書いてしまった場合、悪意あるユーザが「生活’ OR 1=1 OR ‘」というキーワードで検索するとします。すると、発行されるSQLを見て分かるように、OR 1=1で条件が true になるのですべてのタスクが取得できてしまいます。

SELECT "tasks".* FROM "tasks" WHERE (tasks.category = '%生活' OR '%')

これは好きなSQL文を実行できることを意味するので、大切なユーザ情報まで抜き取られてしまい大きな問題へと繋がります。

Railsでは特殊なSQL文字をフィルタする仕組みが組み込まれていて、find メソッドなどのActiveRecordのメソッドでは自動的にこれが適用されます。

検索機能などでWHERE句にユーザの入力値を入れてSQLを発行したい場合は、以下のように検索条件に疑問符を書き、第二引数で検索する値を渡すことでエスケープすることができます。

def search
  keyword = search_params[:search]
  @tasks = Task.where("tasks.category LIKE ?", "%#{keyword}%")
end

複数条件がある場合は以下のように書きます。

def search
  keyword = search_params[:search]
  @tasks = Task.where("tasks.category LIKE ? or tasks.content LIKE ?", "%#{keyword}%", "%#{keyword}%")
end

これにより、「生活’ OR 1=1 OR ‘」というキーワードで検索されても一件もヒットしません。

SELECT "tasks".* FROM "tasks" WHERE (tasks.category LIKE '%生活'' OR 1=1 OR ''%')

LIKE句のサニタイズ (sanitize_sql_like)

SQLにはワイルドカードと呼ばれる特殊な文字があります。主なワイルドカードは「%」と「_」で以下の意味を持ちます。

  • 「%」 任意の長さの文字列(ゼロでも可)
  • 「_」 一文字

なので、検索機能を実装する際、「%」がキーワードとして送られてくるとすべての条件がヒットしてしまいます。

今回は下に示すタスク一覧アプリで挙動を確認していきます。カテゴリーが生活または仕事になっており、タスク-0からタスク-29まで作ってあります。

まずは、検索をするためのsearchメソッドを定義します。

def search
  keyword = search_params[:search]
  @tasks = Task.where("tasks.category LIKE ?", "%#{keyword}%")
end

このメソッドを使って「生活」というキーワードで検索してみます。

カテゴリーが生活のタスクのみ表示されました。次に「%」というキーワードで検索してみます。想定する検索結果は一件もヒットしないことになります。

結果としては、全件ヒットしてしまいました。これは、「%」がSQLでのワイルドカードで任意の長さの文字列を表すからです。そのため、検索キーワードをサニタイズしてあげる必要があります。LIKE句で使うようとして用意されているメソッドがsanitize_sql_likeメソッドになります。これはクラスメソッドなので、下のように使う必要があります。

def search
  keyword = search_params[:search]
  @tasks = Task.where("tasks.category LIKE ?", "%#{Task.sanitize_sql_like(keyword)}%")
end

これにより、検索キーワードで「%」が入力されても「文字としての%」として認識されるため、検索結果は一件もヒットしません。

ちなみにモデルにscopeとして定義する際はTaskを省略できます。

class Task < ApplicationRecord
  scope :search, -> (keyword) { where("tasks.content LIKE(?)", "%#{sanitize_sql_like(keyword)}%") }
end

このように意図しない結果になるのを防ぐため、検索キーワードはサニタイズするようにしましょう。

もぐくん
もぐくん
地味だけどつけるようにしよう

複雑なSQL文で入力値を使う場合

複雑なSQL文でユーザの入力値を使いたい場合は、ActiveRecord::Base#sanitize_sql_arrayを使ってエスケープする方法を使います。

使い方は先程のWHERE句の中に疑問符を書くやり方と同じで、以下のように書きます。

sql = <<~SQL
  SELECT * FROM articles WHERE articles.id IN (?)
SQL

Term.find_in_batches do |terms|
  ActiveRecord::Base.connection.execute(ActiveRecord::Base.sanitize_sql_array([sql, articles.pluck(:id).join(',')]))
end

まとめ

以上、ユーザの入力値をSQLで使用する場合に行うエスケープについてのTipsでした。SQLインジェクションは一度発生させてしまうと一気にアプリの信用を失うことに繋がりかねないので、スタートアップで開発している手前、十分に注意していきたいと思いました

今日はここまでにしたいと思います。読んでいただきありがとうございました。

ABOUT ME
sakai
東京在住の30歳。元々は車部品メーカーで働いていてましたが、プログラミングに興味を持ちスクールに通ってエンジニアになりました。 そこからベンチャー → メガベンチャー → 個人事業主になりました。 最近は生成 AI 関連の業務を中心にやっています。 ヒカルチャンネル(Youtube)とワンピースが大好きです!