SearchLogic と Rails 2.3.10 を利用する際はご用心

先日のyouRoomで発生した間違いメール送信の不具合について調査しましたので、公開いたします。

発端は、Rails2.3.9のセキュリティーアップデートでした。youRoomでは、Rails2.3.9を利用していたためRails2.3.10へのアップデートを行ないました。基本的な動作確認はとれたのでリリースしたのですが、上記のような不具合が残ってしまいました。

原因は、SearchLogicの実装とRails2.3.9からRails2.3.10までのあるコミット(セキュリティー対応以外)にありました。

http://github.com/rails/rails/commit/9b78af95be01534b3f1a4187024d803313c0ee1f

実際には、このような現象が発生しました。

# Group -has_many-> Participation
# Group.joined           ==> defined named_scope
# Group.user_id_not_null ==> genarated named_scope by searchlogic

g = Group.find(1)
# SQL) SELECT * FROM `groups` WHERE (`groups`.`id` = 1)

g.participations
# SQL) SELECT * FROM `participations` WHERE (`participations`.group_id = 1)

g.participations.joined.user_id_not_null
# Acctual SQL) SELECT * FROM `participations` WHERE ((participations.user_id IS NOT NULL) AND (participations.status = 'ACTIVE'))
# Collect SQL) SELECT * FROM `participations` WHERE ((participations.user_id IS NOT NULL) AND ((participations.status = 'ACTIVE') AND (`participations`.group_id = 1)))

g.participations.joined.user_id_not_null  #  Redo
# SQL) SELECT * FROM `participations` WHERE (`participations`.group_id = 1) AND ((participations.user_id IS NOT NULL) AND (participations.status = 'ACTIVE'))

本来は、Acctual SQLの部分でCollect SQLのようにgroup_idで絞ったSQLが発行されるはずですが、実際にはSQLが抜けた状態になりgroupで絞ることができませんでした。このため、正しくない送信先が選ばれることになり、昨日の間違いメールが発生してしまいました。

このような実装で発行されるSQLが上記の様になってしまうのは、Railsを利用している人にとっては非常に恐ろしいところです。さらに、2度目に実行すると正しい結果になるので、非常に見つけにくい現象であると思います。

なぜ、このような現象が発生したのかソースを追って解説します。

g.participations.joined.user_id_not_null
                       ^^^^^^^^^^^^^^^^^

この部分で、本来定義されていないメソッドなので、SearchLogicのmethod_missingに処理が渡ります。

http://github.com/binarylogic/searchlogic/blob/master/lib/searchlogic/active_record/association_proxy.rb

proxy_reflection.klass.send(method, *args)

を解釈すると

Participaion.send('user_id_not_null')

となります。このようにnamed_scopeのChainから一旦抜けたような感じになります。

ここから次にまだメソッドは存在しないので

http://github.com/binarylogic/searchlogic/blob/master/lib/searchlogic/named_scopes/conditions.rb

この辺りで、named_scopeが定義されます。そして、そのメソッドが send で呼び出されます。Line 75 ですね。この時、以下のような呼び出しになります。

Participation.user_id_not_null

ここで、Rails2.3.10の差分が効いてきます。

http://github.com/rails/rails/commit/9b78af95be01534b3f1a4187024d803313c0ee1f
http://github.com/rails/rails/blob/9b78af95be01534b3f1a4187024d803313c0ee1f/activerecord/lib/active_record/named_scope.rb

Rails2.3.10: unless (Scope === proxy_scope || ActiveRecord::Associations::AssociationCollection === proxy_scope)
Rails2.3.9:  unless Scope === proxy_scope

これの差分は、一般的に proxy_scope が AssociationCollection の場合は、同じ Where 区が登録されるのを防ぐために変更されました。

しかし、SearchLogicで動的に生成した場合も上記の様に呼び出した時 proxy_scope が AssociationCollection となります。これまで、group.id の Where区の制限が、with_scope で引き継がれていましたが、引き継がれなくなってしまいます。

もう少し具体的に書くと、正しい場合はnamed_scope.rbのL182あたりで以下の様に呼ばれるのに対して、正しくない場合は一つ上のif文でfalseとなりwith_scopeで引き継がれずに実行されてしまいます。

if current_scoped_methods_when_defined && !scoped_methods.include?(current_scoped_methods_when_defined)
  with_scope current_scoped_methods_when_defined do # ここのwith_scopeで group_id の制限がかかる {:conditions => {:group_id => 1} } という感じ
    proxy_scope.send(method, *args, &block)         # Participation.find ...
  end
else
  proxy_scope.send(method, *args, &block)
 end

そのような流れで、以上なデータが取得され今回の現象が発生してしまったようです。最後の方は、非常に複雑で文章で表現するのは難しく、理解しがたいかと思いますが、現象としては理解いただけるかと思います。

youRoom側の対応としては、Rails2.3.9のセキュリティーの問題も今回のSearchLogicの問題も致命的ですので、一旦、Rails2.3.10の必要なパッチをRails2.3.9に当てる形で対応しています。
http://gist.github.com/632068

この問題は、SearchLogicとRails2.3.10を併用したときのみ発生するようですが、非常に重大ですのでSearchLogicへも連絡しようと思っています。