Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails関連

Ruby on Rails 3.2.15がリリースされました - 変更点まとめ

本日(日本時間2013年10月17日)、Ruby on Rails 3.2.15がリリースされました。3.2系最新リリースになります。
バグ修正の他、1件のセキュリティFIXが含まれています。3.2系ユーザは可能な限り早めにアップデートしましょう。

なお、予定ではこれが3.2系最終リリースで、これ以降はセキュリティFIXのみが提供されることになっています。
バグ修正はされない予定なので、アクティブなRailsプロジェクトは、早めにRails 4への移行を検討したほうが良さそうです。

セキュリティFIX

DoS攻撃につながる可能性のある脆弱性が修正されています。

ActionMailerのLogSubscriberに以下のようなコードが存在していまいしたが、

info("\nSent mail to #{recipients} (%1.fms)" % event.duration)

これはrecipientsに「test%d@example.com」のような文字列を渡すとArgumentErrorが発生します。
また、やり方は思いつきませんでしたが、仮に%1.fmsの部分を無視させることができれば「%10000d」のようにしてログに非常に長い文字列を出力させることが可能になります。
3.2.15では、この脆弱性が修正されました。

なお、本件のみ3.0, 3.1にも対応パッチがリリースされています。
https://groups.google.com/forum/#!topic/ruby-security-ann/yvlR1Vx44c8
4.0系は、この脆弱性は最初から存在しません。

ActionPackの変更点

意図しないIP spoofing attack

HTTPヘッダのCLIENT_IPとX_FORWARDED_FORに別の値をセットすることで、IP spoofing attackを狙う攻撃が存在します。
対策として、ActionDispatch::RemoteIp::GetIp#calculate_ipはこの2つの値が異なる場合に例外を発生させるようになっています。

しかし、一部X_FORWARDED_FORが空でCLIENT_IPのみをセットするプロキシが存在するため、その環境では通常アクセスでもIpSpoofAttackErrorがraiseされてしまっていました。
3.2.15ではこの問題が修正され、HTTP_CLIENT_IPとHTTP_X_FORWARDED_FORの両方がセットされている場合のみIP spoofing attackのチェックをするようになりました。

assert_recognizesのconstraints

assert_recognizesテストで、クエリ文字列にconstraintsを適用するとfailする問題が修正されました。

render partialのformat

以下のようなパターン(通常通りformat = :html)のとき、_myscript.js.erbと_form.html.erbがrenderされることが期待されます。

# app/controllers/users_controller.rb
def index
end

# app/views/users/index.html.erb
<%= render partial: 'myscript', formats: :js %>
<%= render 'form' %>

しかし、3.2.14では_form.js.erbがrenderされていました。
3.2.15ではこの問題が修正され、_form.html.erbがrenderされるようになりました。。

assert_redirected_toの第2引数

ActionDispatch::Assertions::ResponseAssertions#assert_redirected_toの第2引数には、テスト失敗時に表示するメッセージを渡せます。
これが表示されない不具合が修正されました。

ActiveRecordの変更点

inverse_ofその1

ActiveRecordにはinverse_ofという機能があります。

通常のassociationでは、relationを子→親と戻る方向に辿った場合、元のオブジェクトに変更は反映されません。

# app/models/company.rb
class Company < ActiveRecord::Base
  attr_accessible :name
  has_many :users
end

# app/models/user.rb
class User < ActiveRecord::Base
  attr_accessible :name, :company
  belongs_to :company
end

# rails console
# 初期データ
User.create(name: '太郎', company: Company.create(name: '会社1'))

company = Company.first
compnay.name # => '会社1'
user = company.users.first
user.company.name = '会社2'
company.name # => '会社1'

inverse_ofをセットすると、これが反映されるようになります。

# app/models/company.rb
class Company < ActiveRecord::Base
  attr_accessible :name
  has_many :users, inverse_of: :company
end

# app/models/user.rb
class User < ActiveRecord::Base
  attr_accessible :name, :company
  belongs_to :company, inverse_of: :users
end

# rails console
company = Company.first
compnay.name # => '会社1'
user = company.users.first
user.company.name = '会社2'
company.name # => '会社2'

この便利なinverse_ofですが、collection_proxyに対してfind_or_initialize_by_*で呼び出し、DBにレコードが存在しない場合に適用されないバグがありました。

# rails console
company = Company.first
compnay.name # => '会社1'
user = company.users.find_or_initialize_by_name('太郎') # DBに存在した
user.company.name = '会社2'
company.name # => '会社1' ★バグ!

3.2.15ではこの問題が修正され、「会社2」が取得できるようになりました。

※手元で確認したところ、この問題は4.0.0でも発生しました。あとuser = company.users.active.firstのようにscopeを付けるとやはり反映されないですが、これは正常なのかなあ。

inverse_ofその2

上記の例で、Userに以下のようなコールバックを追加します。companyを参照しているのがポイントです。
この状態で、company.usersにUserを追加します。

# app/models/user.rb
class User < ActiveRecord::Base
  attr_accessible :name, :company
  belongs_to :company, inverse_of: :users

  after_save do
    puts "User created in company #{company.name}"
  end
end

# rails console
company = Company.first
company.users << User.new(name: '次郎')
# INSERT INTO "users" ("company_id", "created_at", "name", "updated_at") VALUES (?, ?, ?, ?)  [["company_id", 1], ["created_at", Thu, 17 Oct 2013 05:30:21 UTC +00:00], ["name", "じろう"], ["updated_at", Thu, 17 Oct 2013 05:30:21 UTC +00:00]]
# SELECT "companies".* FROM "companies" WHERE "companies"."id" = 1 LIMIT 1
# User created in company 会社1

after_saveのところで、CompanyをSELECTするSQLが走っています。
inverse_ofされているのだから、UserからCompanyは既にLOADされたものが見えるはずで、これは無駄です。

3.2.15では、この無駄なSELECTが実行されないようになりました。

引数付last(N)の発行するSQL

FinderMethods#lastは、Company.lastのように引数無しで実行するほかに、Company.last(5)のように最後から5個を配列で取得する使い方ができます。
このとき発行されるSQLが、以下のように変更されました。

Company.joins(:users).last(5)

# Rails 3.2.14
# => SELECT "companies".* FROM "companies" INNER JOIN "users" ON "users"."company_id" = "companies"."id" ORDER BY id DESC LIMIT 5

# Rails 3.2.15
# => SELECT "companies".* FROM "companies" INNER JOIN "users" ON "users"."company_id" = "companies"."id" ORDER BY "companies"."id" DESC LIMIT 5

3.2.14では、joinしたときにprimary keyが衝突することがありましたが、これが修正されました。

fixture読み込み

fixtures読み込みで、ディレクトリへのsymbolic linkが参照されるようになりました。

lock_versionを使うときのquote_value

lock_versionカラムを使うと簡単に楽観的ロックを実現できます。
このとき、内部的に使われるquote_valueに、カラムの型(この場合数値型であるべき)情報が渡されていませんでした。
これにより、一部の環境(JRuby + activerecord-jdbcmssql-adapter + SQLServer 2000など)で、不適切なクオート処理がされエラーになることがあったようです。
3.2.15ではこの問題が修正され、lock_versionをquoteする処理に型情報が付与されるようになったので、エラーは発生しなくなりました。

ActiveSupportの変更点

ActiveSupport::Cache::FileStore#cleanup

ActiveSupport::Cache::FileStore#cleanupが、each_keyメソッドに依存して正しく動作していませんでした。
3.2.15ではこれが修正され、cleanupを呼ぶと正しく期限切れキャッシュが削除されるようになりました。

TaggedLogging

TaggedLoggingがrespond_to_missing?を実装しました。これは@loggerのrespond_to?に委譲されます。
特別に機能が変わるわけではありませんが、method_missingを使う際のベストプラクティスが適用された形になります。

その他

ActionMailer, ActionPack, ActiveModel, ActiveResource, Railtiesには、機能変更はありません。

まとめ

以上、CHANGELOGとコミットログをまとめてみました。
地味に影響範囲が大きいバグが多数含まれているので、しっかりテストしつつ早めにアップデートしたいですね。

またRails 4のパッチリリースがなかなかでないのが地味に気になりますが、3.2系もいよいよ終盤なので、より積極的にRails 4を使っていきたいです。


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。