aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--actionpack/CHANGELOG.md22
-rw-r--r--actionpack/lib/action_controller/log_subscriber.rb9
-rw-r--r--actionpack/lib/action_dispatch.rb10
-rw-r--r--actionpack/lib/action_dispatch/middleware/cookies.rb16
-rw-r--r--actionpack/lib/action_dispatch/middleware/reloader.rb13
-rw-r--r--actionpack/lib/action_dispatch/middleware/session/json_serializer.rb13
-rw-r--r--actionpack/lib/action_dispatch/middleware/session/marshal_serializer.rb14
-rw-r--r--actionpack/lib/action_dispatch/request/utils.rb13
-rw-r--r--actionpack/test/dispatch/cookies_test.rb33
-rw-r--r--activerecord/CHANGELOG.md40
-rw-r--r--activerecord/lib/active_record/attribute_methods.rb26
-rw-r--r--activerecord/lib/active_record/dynamic_matchers.rb8
-rw-r--r--activerecord/lib/active_record/enum.rb46
-rw-r--r--activerecord/lib/active_record/persistence.rb2
-rw-r--r--activerecord/lib/active_record/relation/batches.rb11
-rw-r--r--activerecord/lib/active_record/relation/query_methods.rb6
-rw-r--r--activerecord/lib/active_record/scoping/named.rb6
-rw-r--r--activerecord/test/cases/batches_test.rb13
-rw-r--r--activerecord/test/cases/enum_test.rb59
-rw-r--r--activerecord/test/cases/finder_respond_to_test.rb5
-rw-r--r--activerecord/test/cases/locking_test.rb11
-rw-r--r--activerecord/test/cases/relation/mutation_test.rb4
-rw-r--r--activerecord/test/cases/relations_test.rb31
-rw-r--r--activerecord/test/cases/scoping/named_scoping_test.rb57
-rw-r--r--activesupport/CHANGELOG.md7
-rw-r--r--activesupport/lib/active_support/message_encryptor.rb2
-rw-r--r--guides/source/4_1_release_notes.md3
-rw-r--r--guides/source/action_controller_overview.md30
-rw-r--r--guides/source/configuring.md4
-rw-r--r--guides/source/security.md43
-rw-r--r--railties/lib/rails/application.rb3
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/initializers/session_store.rb.tt2
-rw-r--r--railties/test/generators/app_generator_test.rb2
33 files changed, 535 insertions, 29 deletions
diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md
index 23bb01d678..f836b69042 100644
--- a/actionpack/CHANGELOG.md
+++ b/actionpack/CHANGELOG.md
@@ -1,3 +1,17 @@
+* Add `:serializer` option for `config.session_store :cookie_store`. This
+ changes default serializer when using `:cookie_store` to
+ `ActionDispatch::Session::MarshalSerializer` which is wrapper on Marshal.
+
+ It is also possible to pass:
+
+ * `:json_serializer` which is secure wrapper on JSON using `JSON.parse` and
+ `JSON.generate` methods with quirks mode;
+ * any other Symbol or String like `:my_custom_serializer` which will be
+ camelized and constantized in `ActionDispatch::Session` namespace;
+ * serializer object with `load` and `dump` methods defined.
+
+ *Łukasz Sarnacki + Matt Aimonetti*
+
* Ensure that `request.filtered_parameters` is reset between calls to `process`
in `ActionController::TestCase`.
@@ -11,6 +25,14 @@
*Maurizio De Santis*
+* Log which keys were affected by deep munge.
+
+ Deep munge solves CVE-2013-0155 security vulnerability, but its
+ behaviour is definately confusing, so now at least information
+ about for which keys values were set to nil is visible in logs.
+
+ *Łukasz Sarnacki*
+
* Automatically convert dashes to underscores for shorthand routes, e.g:
get '/our-work/latest'
diff --git a/actionpack/lib/action_controller/log_subscriber.rb b/actionpack/lib/action_controller/log_subscriber.rb
index 9279d8bcea..823a1050b5 100644
--- a/actionpack/lib/action_controller/log_subscriber.rb
+++ b/actionpack/lib/action_controller/log_subscriber.rb
@@ -53,6 +53,15 @@ module ActionController
debug("Unpermitted parameters: #{unpermitted_keys.join(", ")}")
end
+ def deep_munge(event)
+ message = "Value for params[:#{event.payload[:keys].join('][:')}] was set"\
+ "to nil, because it was one of [], [null] or [null, null, ...]."\
+ "Go to http://guides.rubyonrails.org/security.html#unsafe-query-generation"\
+ "for more information."\
+
+ debug(message)
+ end
+
%w(write_fragment read_fragment exist_fragment?
expire_fragment expire_page write_page).each do |method|
class_eval <<-METHOD, __FILE__, __LINE__ + 1
diff --git a/actionpack/lib/action_dispatch.rb b/actionpack/lib/action_dispatch.rb
index 920e651b08..36dcca2905 100644
--- a/actionpack/lib/action_dispatch.rb
+++ b/actionpack/lib/action_dispatch.rb
@@ -82,10 +82,12 @@ module ActionDispatch
end
module Session
- autoload :AbstractStore, 'action_dispatch/middleware/session/abstract_store'
- autoload :CookieStore, 'action_dispatch/middleware/session/cookie_store'
- autoload :MemCacheStore, 'action_dispatch/middleware/session/mem_cache_store'
- autoload :CacheStore, 'action_dispatch/middleware/session/cache_store'
+ autoload :AbstractStore, 'action_dispatch/middleware/session/abstract_store'
+ autoload :CookieStore, 'action_dispatch/middleware/session/cookie_store'
+ autoload :MemCacheStore, 'action_dispatch/middleware/session/mem_cache_store'
+ autoload :CacheStore, 'action_dispatch/middleware/session/cache_store'
+ autoload :JsonSerializer, 'action_dispatch/middleware/session/json_serializer'
+ autoload :MarshalSerializer, 'action_dispatch/middleware/session/marshal_serializer'
end
mattr_accessor :test_app
diff --git a/actionpack/lib/action_dispatch/middleware/cookies.rb b/actionpack/lib/action_dispatch/middleware/cookies.rb
index fe110d7938..f9f034952e 100644
--- a/actionpack/lib/action_dispatch/middleware/cookies.rb
+++ b/actionpack/lib/action_dispatch/middleware/cookies.rb
@@ -89,6 +89,7 @@ module ActionDispatch
ENCRYPTED_SIGNED_COOKIE_SALT = "action_dispatch.encrypted_signed_cookie_salt".freeze
SECRET_TOKEN = "action_dispatch.secret_token".freeze
SECRET_KEY_BASE = "action_dispatch.secret_key_base".freeze
+ SESSION_SERIALIZER = "action_dispatch.session_serializer".freeze
# Cookies can typically store 4096 bytes.
MAX_COOKIE_SIZE = 4096
@@ -210,7 +211,8 @@ module ActionDispatch
encrypted_signed_cookie_salt: env[ENCRYPTED_SIGNED_COOKIE_SALT] || '',
secret_token: env[SECRET_TOKEN],
secret_key_base: env[SECRET_KEY_BASE],
- upgrade_legacy_signed_cookies: env[SECRET_TOKEN].present? && env[SECRET_KEY_BASE].present?
+ upgrade_legacy_signed_cookies: env[SECRET_TOKEN].present? && env[SECRET_KEY_BASE].present?,
+ session_serializer: env[SESSION_SERIALIZER]
}
end
@@ -435,7 +437,7 @@ module ActionDispatch
@options = options
secret = key_generator.generate_key(@options[:encrypted_cookie_salt])
sign_secret = key_generator.generate_key(@options[:encrypted_signed_cookie_salt])
- @encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret)
+ @encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret, serializer: serializer)
end
def [](name)
@@ -462,6 +464,16 @@ module ActionDispatch
rescue ActiveSupport::MessageVerifier::InvalidSignature, ActiveSupport::MessageEncryptor::InvalidMessage
nil
end
+
+ def serializer
+ serializer = @options[:session_serializer] || :marshal_serializer
+ case serializer
+ when Symbol, String
+ ActionDispatch::Session.const_get(serializer.to_s.camelize)
+ else
+ serializer
+ end
+ end
end
# UpgradeLegacyEncryptedCookieJar is used by ActionDispatch::Session::CookieStore
diff --git a/actionpack/lib/action_dispatch/middleware/reloader.rb b/actionpack/lib/action_dispatch/middleware/reloader.rb
index 2f6968eb2e..15b5a48535 100644
--- a/actionpack/lib/action_dispatch/middleware/reloader.rb
+++ b/actionpack/lib/action_dispatch/middleware/reloader.rb
@@ -1,3 +1,5 @@
+require 'active_support/deprecation/reporting'
+
module ActionDispatch
# ActionDispatch::Reloader provides prepare and cleanup callbacks,
# intended to assist with code reloading during development.
@@ -25,19 +27,26 @@ module ActionDispatch
#
class Reloader
include ActiveSupport::Callbacks
+ include ActiveSupport::Deprecation::Reporting
- define_callbacks :prepare, :scope => :name
- define_callbacks :cleanup, :scope => :name
+ define_callbacks :prepare
+ define_callbacks :cleanup
# Add a prepare callback. Prepare callbacks are run before each request, prior
# to ActionDispatch::Callback's before callbacks.
def self.to_prepare(*args, &block)
+ unless block_given?
+ warn "to_prepare without a block is deprecated. Please use a block"
+ end
set_callback(:prepare, *args, &block)
end
# Add a cleanup callback. Cleanup callbacks are run after each request is
# complete (after #close is called on the response body).
def self.to_cleanup(*args, &block)
+ unless block_given?
+ warn "to_cleanup without a block is deprecated. Please use a block"
+ end
set_callback(:cleanup, *args, &block)
end
diff --git a/actionpack/lib/action_dispatch/middleware/session/json_serializer.rb b/actionpack/lib/action_dispatch/middleware/session/json_serializer.rb
new file mode 100644
index 0000000000..d341853f7a
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/session/json_serializer.rb
@@ -0,0 +1,13 @@
+module ActionDispatch
+ module Session
+ class JsonSerializer
+ def self.load(value)
+ JSON.parse(value, quirks_mode: true)
+ end
+
+ def self.dump(value)
+ JSON.generate(value, quirks_mode: true)
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/middleware/session/marshal_serializer.rb b/actionpack/lib/action_dispatch/middleware/session/marshal_serializer.rb
new file mode 100644
index 0000000000..26622f682d
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/session/marshal_serializer.rb
@@ -0,0 +1,14 @@
+module ActionDispatch
+ module Session
+ class MarshalSerializer
+ def self.load(value)
+ Marshal.load(value)
+ end
+
+ def self.dump(value)
+ Marshal.dump(value)
+ end
+ end
+ end
+end
+
diff --git a/actionpack/lib/action_dispatch/request/utils.rb b/actionpack/lib/action_dispatch/request/utils.rb
index a6dca9741c..9d4f1aa3c5 100644
--- a/actionpack/lib/action_dispatch/request/utils.rb
+++ b/actionpack/lib/action_dispatch/request/utils.rb
@@ -7,18 +7,23 @@ module ActionDispatch
class << self
# Remove nils from the params hash
- def deep_munge(hash)
+ def deep_munge(hash, keys = [])
return hash unless perform_deep_munge
hash.each do |k, v|
+ keys << k
case v
when Array
- v.grep(Hash) { |x| deep_munge(x) }
+ v.grep(Hash) { |x| deep_munge(x, keys) }
v.compact!
- hash[k] = nil if v.empty?
+ if v.empty?
+ hash[k] = nil
+ ActiveSupport::Notifications.instrument("deep_munge.action_controller", keys: keys)
+ end
when Hash
- deep_munge(v)
+ deep_munge(v, keys)
end
+ keys.pop
end
hash
diff --git a/actionpack/test/dispatch/cookies_test.rb b/actionpack/test/dispatch/cookies_test.rb
index 91ac13e7c6..b19ce905f5 100644
--- a/actionpack/test/dispatch/cookies_test.rb
+++ b/actionpack/test/dispatch/cookies_test.rb
@@ -379,6 +379,39 @@ class CookiesTest < ActionController::TestCase
assert_equal 'bar', cookies.encrypted[:foo]
end
+ class ActionDispatch::Session::CustomJsonSerializer
+ def self.load(value)
+ JSON.load(value) + " and loaded"
+ end
+
+ def self.dump(value)
+ JSON.dump(value + " was dumped")
+ end
+ end
+
+ def test_encrypted_cookie_using_custom_json_serializer
+ @request.env["action_dispatch.session_serializer"] = :custom_json_serializer
+ get :set_encrypted_cookie
+ assert_equal 'bar was dumped and loaded', cookies.encrypted[:foo]
+ end
+
+ def test_encrypted_cookie_using_serializer_object
+ @request.env["action_dispatch.session_serializer"] = ActionDispatch::Session::CustomJsonSerializer
+ get :set_encrypted_cookie
+ assert_equal 'bar was dumped and loaded', cookies.encrypted[:foo]
+ end
+
+ def test_encrypted_cookie_using_json_serializer
+ @request.env["action_dispatch.session_serializer"] = :json_serializer
+ get :set_encrypted_cookie
+ cookies = @controller.send :cookies
+ assert_not_equal 'bar', cookies[:foo]
+ assert_raises TypeError do
+ cookies.signed[:foo]
+ end
+ assert_equal 'bar', cookies.encrypted[:foo]
+ end
+
def test_accessing_nonexistant_encrypted_cookie_should_not_raise_invalid_message
get :set_encrypted_cookie
assert_nil @controller.send(:cookies).encrypted[:non_existant_attribute]
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md
index 7db99d4aeb..fe0d7b2b35 100644
--- a/activerecord/CHANGELOG.md
+++ b/activerecord/CHANGELOG.md
@@ -1,3 +1,43 @@
+* `find_in_batches` now returns an `Enumerator` when called without a block, so that it
+ can be chained with other `Enumerable` methods.
+
+ *Marc-André Lafortune*
+
+* `enum` now raises on "dangerous" name conflicts
+
+ Dangerous name conflicts includes instance or class method conflicts
+ with methods defined within `ActiveRecord::Base` but not its ancestors,
+ as well as conflicts with methods generated by other enums on the same
+ class.
+
+ Fixes #13389.
+
+ *Godfrey Chan*
+
+* `scope` now raises on "dangerous" name conflicts
+
+ Similar to dangerous attribute methods, a scope name conflict is
+ dangerous if it conflicts with an existing class method defined within
+ `ActiveRecord::Base` but not its ancestors.
+
+ See also #13389.
+
+ *Godfrey Chan*, *Philippe Creux*
+
+* Correctly send an user provided statement to a `lock!()` call.
+
+ person.lock! 'FOR SHARE NOWAIT'
+ # Before: SELECT * ... LIMIT 1 FOR UPDATE
+ # After: SELECT * ... LIMIT 1 FOR SHARE NOWAIT
+
+ Fixes #13788.
+
+ *Maurício Linhares*
+
+* Handle aliased attributes `select()`, `order()` and `reorder()`.
+
+ *Tsutomu Kuroda*
+
* Reset the collection association when calling `reset` on it.
Before:
diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb
index 73761520f7..ccbff8d1ff 100644
--- a/activerecord/lib/active_record/attribute_methods.rb
+++ b/activerecord/lib/active_record/attribute_methods.rb
@@ -110,16 +110,34 @@ module ActiveRecord
end
end
- # A method name is 'dangerous' if it is already defined by Active Record, but
+ # A method name is 'dangerous' if it is already (re)defined by Active Record, but
# not by any ancestors. (So 'puts' is not dangerous but 'save' is.)
def dangerous_attribute_method?(name) # :nodoc:
method_defined_within?(name, Base)
end
- def method_defined_within?(name, klass, sup = klass.superclass) # :nodoc:
+ def method_defined_within?(name, klass, superklass = klass.superclass) # :nodoc:
if klass.method_defined?(name) || klass.private_method_defined?(name)
- if sup.method_defined?(name) || sup.private_method_defined?(name)
- klass.instance_method(name).owner != sup.instance_method(name).owner
+ if superklass.method_defined?(name) || superklass.private_method_defined?(name)
+ klass.instance_method(name).owner != superklass.instance_method(name).owner
+ else
+ true
+ end
+ else
+ false
+ end
+ end
+
+ # A class method is 'dangerous' if it is already (re)defined by Active Record, but
+ # not by any ancestors. (So 'puts' is not dangerous but 'new' is.)
+ def dangerous_class_method?(method_name)
+ class_method_defined_within?(method_name, Base)
+ end
+
+ def class_method_defined_within?(name, klass, superklass = klass.superclass) # :nodoc
+ if klass.respond_to?(name, true)
+ if superklass.respond_to?(name, true)
+ klass.method(name).owner != superklass.method(name).owner
else
true
end
diff --git a/activerecord/lib/active_record/dynamic_matchers.rb b/activerecord/lib/active_record/dynamic_matchers.rb
index 5caab09038..e94b74063e 100644
--- a/activerecord/lib/active_record/dynamic_matchers.rb
+++ b/activerecord/lib/active_record/dynamic_matchers.rb
@@ -6,8 +6,12 @@ module ActiveRecord
# then we can remove the indirection.
def respond_to?(name, include_private = false)
- match = Method.match(self, name)
- match && match.valid? || super
+ if self == Base
+ super
+ else
+ match = Method.match(self, name)
+ match && match.valid? || super
+ end
end
private
diff --git a/activerecord/lib/active_record/enum.rb b/activerecord/lib/active_record/enum.rb
index 53dde5e564..059bfe9a0f 100644
--- a/activerecord/lib/active_record/enum.rb
+++ b/activerecord/lib/active_record/enum.rb
@@ -77,10 +77,12 @@ module ActiveRecord
name = name.to_sym
# def self.statuses statuses end
+ detect_enum_conflict!(name, name.to_s.pluralize, true)
klass.singleton_class.send(:define_method, name.to_s.pluralize) { enum_values }
_enum_methods_module.module_eval do
# def status=(value) self[:status] = statuses[value] end
+ klass.send(:detect_enum_conflict!, name, "#{name}=")
define_method("#{name}=") { |value|
if enum_values.has_key?(value) || value.blank?
self[name] = enum_values[value]
@@ -95,23 +97,28 @@ module ActiveRecord
}
# def status() statuses.key self[:status] end
+ klass.send(:detect_enum_conflict!, name, name)
define_method(name) { enum_values.key self[name] }
# def status_before_type_cast() statuses.key self[:status] end
+ klass.send(:detect_enum_conflict!, name, "#{name}_before_type_cast")
define_method("#{name}_before_type_cast") { enum_values.key self[name] }
pairs = values.respond_to?(:each_pair) ? values.each_pair : values.each_with_index
pairs.each do |value, i|
enum_values[value] = i
- # scope :active, -> { where status: 0 }
- klass.scope value, -> { klass.where name => i }
-
# def active?() status == 0 end
+ klass.send(:detect_enum_conflict!, name, "#{value}?")
define_method("#{value}?") { self[name] == i }
# def active!() update! status: :active end
+ klass.send(:detect_enum_conflict!, name, "#{value}!")
define_method("#{value}!") { update! name => value }
+
+ # scope :active, -> { where status: 0 }
+ klass.send(:detect_enum_conflict!, name, value, true)
+ klass.scope value, -> { klass.where name => i }
end
DEFINED_ENUMS[name.to_s] = enum_values
@@ -148,5 +155,38 @@ module ActiveRecord
mod
end
end
+
+ ENUM_CONFLICT_MESSAGE = \
+ "You tried to define an enum named \"%{enum}\" on the model \"%{klass}\", but " \
+ "this will generate a %{type} method \"%{method}\", which is already defined " \
+ "by %{source}."
+
+ def detect_enum_conflict!(enum_name, method_name, klass_method = false)
+ if klass_method && dangerous_class_method?(method_name)
+ raise ArgumentError, ENUM_CONFLICT_MESSAGE % {
+ enum: enum_name,
+ klass: self.name,
+ type: 'class',
+ method: method_name,
+ source: 'Active Record'
+ }
+ elsif !klass_method && dangerous_attribute_method?(method_name)
+ raise ArgumentError, ENUM_CONFLICT_MESSAGE % {
+ enum: enum_name,
+ klass: self.name,
+ type: 'instance',
+ method: method_name,
+ source: 'Active Record'
+ }
+ elsif !klass_method && method_defined_within?(method_name, _enum_methods_module, Module)
+ raise ArgumentError, ENUM_CONFLICT_MESSAGE % {
+ enum: enum_name,
+ klass: self.name,
+ type: 'instance',
+ method: method_name,
+ source: 'another enum'
+ }
+ end
+ end
end
end
diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb
index 460fbdb3f8..b1b35ed940 100644
--- a/activerecord/lib/active_record/persistence.rb
+++ b/activerecord/lib/active_record/persistence.rb
@@ -389,7 +389,7 @@ module ActiveRecord
fresh_object =
if options && options[:lock]
- self.class.unscoped { self.class.lock.find(id) }
+ self.class.unscoped { self.class.lock(options[:lock]).find(id) }
else
self.class.unscoped { self.class.find(id) }
end
diff --git a/activerecord/lib/active_record/relation/batches.rb b/activerecord/lib/active_record/relation/batches.rb
index f02e2365f7..dfcfef2ad2 100644
--- a/activerecord/lib/active_record/relation/batches.rb
+++ b/activerecord/lib/active_record/relation/batches.rb
@@ -64,6 +64,16 @@ module ActiveRecord
# group.each { |person| person.party_all_night! }
# end
#
+ # If you do not provide a block to #find_in_batches, it will return an Enumerator
+ # for chaining with other methods:
+ #
+ # Person.find_in_batches.with_index do |group, batch|
+ # puts "Processing group ##{batch}"
+ # group.each(&:recover_from_last_night!)
+ # end
+ #
+ # To be yielded each record one by one, use #find_each instead.
+ #
# ==== Options
# * <tt>:batch_size</tt> - Specifies the size of the batch. Default to 1000.
# * <tt>:start</tt> - Specifies the starting point for the batch processing.
@@ -86,6 +96,7 @@ module ActiveRecord
# the batch sizes.
def find_in_batches(options = {})
options.assert_valid_keys(:start, :batch_size)
+ return to_enum(:find_in_batches, options) unless block_given?
relation = self
diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb
index 993f628fa3..88fc47fada 100644
--- a/activerecord/lib/active_record/relation/query_methods.rb
+++ b/activerecord/lib/active_record/relation/query_methods.rb
@@ -234,7 +234,9 @@ module ActiveRecord
def select!(*fields) # :nodoc:
fields.flatten!
-
+ fields.map! do |field|
+ klass.attribute_alias?(field) ? klass.attribute_alias(field).to_sym : field
+ end
self.select_values += fields
self
end
@@ -1048,9 +1050,11 @@ module ActiveRecord
order_args.map! do |arg|
case arg
when Symbol
+ arg = klass.attribute_alias(arg).to_sym if klass.attribute_alias?(arg)
table[arg].asc
when Hash
arg.map { |field, dir|
+ field = klass.attribute_alias(field).to_sym if klass.attribute_alias?(field)
table[field].send(dir)
}
else
diff --git a/activerecord/lib/active_record/scoping/named.rb b/activerecord/lib/active_record/scoping/named.rb
index 2a5718f388..49cadb66d0 100644
--- a/activerecord/lib/active_record/scoping/named.rb
+++ b/activerecord/lib/active_record/scoping/named.rb
@@ -139,6 +139,12 @@ module ActiveRecord
# Article.published.featured.latest_article
# Article.featured.titles
def scope(name, body, &block)
+ if dangerous_class_method?(name)
+ raise ArgumentError, "You tried to define a scope named \"#{name}\" " \
+ "on the model \"#{self.name}\", but Active Record already defined " \
+ "a class method with the same name."
+ end
+
extension = Module.new(&block) if block
singleton_class.send(:define_method, name) do |*args|
diff --git a/activerecord/test/cases/batches_test.rb b/activerecord/test/cases/batches_test.rb
index ebb36e4940..1161b57514 100644
--- a/activerecord/test/cases/batches_test.rb
+++ b/activerecord/test/cases/batches_test.rb
@@ -172,4 +172,17 @@ class EachTest < ActiveRecord::TestCase
end
end
end
+
+ def test_find_in_batches_should_return_an_enumerator
+ enum = nil
+ assert_queries(0) do
+ enum = Post.find_in_batches(:batch_size => 1)
+ end
+ assert_queries(4) do
+ enum.first(4) do |batch|
+ assert_kind_of Array, batch
+ assert_kind_of Post, batch.first
+ end
+ end
+ end
end
diff --git a/activerecord/test/cases/enum_test.rb b/activerecord/test/cases/enum_test.rb
index 8719f45e76..5cac630a3a 100644
--- a/activerecord/test/cases/enum_test.rb
+++ b/activerecord/test/cases/enum_test.rb
@@ -163,4 +163,63 @@ class EnumTest < ActiveRecord::TestCase
test "_before_type_cast returns the enum label (required for form fields)" do
assert_equal "proposed", @book.status_before_type_cast
end
+
+ test "reserved enum names" do
+ klass = Class.new(ActiveRecord::Base) do
+ self.table_name = "books"
+ enum status: [:proposed, :written, :published]
+ end
+
+ conflicts = [
+ :column, # generates class method .columns, which conflicts with an AR method
+ :logger, # generates #logger, which conflicts with an AR method
+ :attributes, # generates #attributes=, which conflicts with an AR method
+ ]
+
+ conflicts.each_with_index do |name, i|
+ assert_raises(ArgumentError, "enum name `#{name}` should not be allowed") do
+ klass.class_eval { enum name => ["value_#{i}"] }
+ end
+ end
+ end
+
+ test "reserved enum values" do
+ klass = Class.new(ActiveRecord::Base) do
+ self.table_name = "books"
+ enum status: [:proposed, :written, :published]
+ end
+
+ conflicts = [
+ :new, # generates a scope that conflicts with an AR class method
+ :valid, # generates #valid?, which conflicts with an AR method
+ :save, # generates #save!, which conflicts with an AR method
+ :proposed, # same value as an existing enum
+ ]
+
+ conflicts.each_with_index do |value, i|
+ assert_raises(ArgumentError, "enum value `#{value}` should not be allowed") do
+ klass.class_eval { enum "status_#{i}" => [value] }
+ end
+ end
+ end
+
+ test "overriding enum method should not raise" do
+ assert_nothing_raised do
+ klass = Class.new(ActiveRecord::Base) do
+ self.table_name = "books"
+
+ def published!
+ super
+ "do publish work..."
+ end
+
+ enum status: [:proposed, :written, :published]
+
+ def written!
+ super
+ "do written work..."
+ end
+ end
+ end
+ end
end
diff --git a/activerecord/test/cases/finder_respond_to_test.rb b/activerecord/test/cases/finder_respond_to_test.rb
index 3ff22f222f..6ab2657c44 100644
--- a/activerecord/test/cases/finder_respond_to_test.rb
+++ b/activerecord/test/cases/finder_respond_to_test.rb
@@ -5,6 +5,11 @@ class FinderRespondToTest < ActiveRecord::TestCase
fixtures :topics
+ def test_should_preserve_normal_respond_to_behaviour_on_base
+ assert_respond_to ActiveRecord::Base, :new
+ assert !ActiveRecord::Base.respond_to?(:find_by_something)
+ end
+
def test_should_preserve_normal_respond_to_behaviour_and_respond_to_newly_added_method
class << Topic; self; end.send(:define_method, :method_added_for_finder_respond_to_test) { }
assert_respond_to Topic, :method_added_for_finder_respond_to_test
diff --git a/activerecord/test/cases/locking_test.rb b/activerecord/test/cases/locking_test.rb
index a16ed963fe..c373dc1511 100644
--- a/activerecord/test/cases/locking_test.rb
+++ b/activerecord/test/cases/locking_test.rb
@@ -431,6 +431,17 @@ unless current_adapter?(:SybaseAdapter, :OpenBaseAdapter) || in_memory_db?
assert_equal old, person.reload.first_name
end
+ if current_adapter?(:PostgreSQLAdapter)
+ def test_lock_sending_custom_lock_statement
+ Person.transaction do
+ person = Person.find(1)
+ assert_sql(/LIMIT 1 FOR SHARE NOWAIT/) do
+ person.lock!('FOR SHARE NOWAIT')
+ end
+ end
+ end
+ end
+
if current_adapter?(:PostgreSQLAdapter, :OracleAdapter)
def test_no_locks_no_wait
first, second = duel { Person.find 1 }
diff --git a/activerecord/test/cases/relation/mutation_test.rb b/activerecord/test/cases/relation/mutation_test.rb
index 7cb2a19bee..4fafa668fb 100644
--- a/activerecord/test/cases/relation/mutation_test.rb
+++ b/activerecord/test/cases/relation/mutation_test.rb
@@ -14,6 +14,10 @@ module ActiveRecord
def relation_delegate_class(klass)
self.class.relation_delegate_class(klass)
end
+
+ def attribute_alias?(name)
+ false
+ end
end
def relation
diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb
index e874c93110..e390d37871 100644
--- a/activerecord/test/cases/relations_test.rb
+++ b/activerecord/test/cases/relations_test.rb
@@ -206,12 +206,36 @@ class RelationTest < ActiveRecord::TestCase
assert_equal topics(:fourth).title, topics.first.title
end
+ def test_finding_with_order_by_aliased_attributes
+ topics = Topic.order(:heading)
+ assert_equal 5, topics.to_a.size
+ assert_equal topics(:fifth).title, topics.first.title
+ end
+
+ def test_finding_with_assoc_order_by_aliased_attributes
+ topics = Topic.order(heading: :desc)
+ assert_equal 5, topics.to_a.size
+ assert_equal topics(:third).title, topics.first.title
+ end
+
def test_finding_with_reorder
topics = Topic.order('author_name').order('title').reorder('id').to_a
topics_titles = topics.map{ |t| t.title }
assert_equal ['The First Topic', 'The Second Topic of the day', 'The Third Topic of the day', 'The Fourth Topic of the day', 'The Fifth Topic of the day'], topics_titles
end
+ def test_finding_with_reorder_by_aliased_attributes
+ topics = Topic.order('author_name').reorder(:heading)
+ assert_equal 5, topics.to_a.size
+ assert_equal topics(:fifth).title, topics.first.title
+ end
+
+ def test_finding_with_assoc_reorder_by_aliased_attributes
+ topics = Topic.order('author_name').reorder(heading: :desc)
+ assert_equal 5, topics.to_a.size
+ assert_equal topics(:third).title, topics.first.title
+ end
+
def test_finding_with_order_and_take
entrants = Entrant.order("id ASC").limit(2).to_a
@@ -775,6 +799,13 @@ class RelationTest < ActiveRecord::TestCase
assert_equal david.salary, developer.salary
end
+ def test_select_takes_an_aliased_attribute
+ first = topics(:first)
+
+ topic = Topic.where(id: first.id).select(:heading).first
+ assert_equal first.heading, topic.heading
+ end
+
def test_select_argument_error
assert_raises(ArgumentError) { Developer.select }
end
diff --git a/activerecord/test/cases/scoping/named_scoping_test.rb b/activerecord/test/cases/scoping/named_scoping_test.rb
index 086977d9a2..9dc26cfd4d 100644
--- a/activerecord/test/cases/scoping/named_scoping_test.rb
+++ b/activerecord/test/cases/scoping/named_scoping_test.rb
@@ -266,6 +266,63 @@ class NamedScopingTest < ActiveRecord::TestCase
assert_equal 'lifo', topic.author_name
end
+ def test_reserved_scope_names
+ klass = Class.new(ActiveRecord::Base) do
+ self.table_name = "topics"
+
+ scope :approved, -> { where(approved: true) }
+
+ class << self
+ public
+ def pub; end
+
+ private
+ def pri; end
+
+ protected
+ def pro; end
+ end
+ end
+
+ subklass = Class.new(klass)
+
+ conflicts = [
+ :create, # public class method on AR::Base
+ :relation, # private class method on AR::Base
+ :new, # redefined class method on AR::Base
+ :all, # a default scope
+ ]
+
+ non_conflicts = [
+ :find_by_title, # dynamic finder method
+ :approved, # existing scope
+ :pub, # existing public class method
+ :pri, # existing private class method
+ :pro, # existing protected class method
+ :open, # a ::Kernel method
+ ]
+
+ conflicts.each do |name|
+ assert_raises(ArgumentError, "scope `#{name}` should not be allowed") do
+ klass.class_eval { scope name, ->{ where(approved: true) } }
+ end
+
+ assert_raises(ArgumentError, "scope `#{name}` should not be allowed") do
+ subklass.class_eval { scope name, ->{ where(approved: true) } }
+ end
+ end
+
+ non_conflicts.each do |name|
+ assert_nothing_raised do
+ klass.class_eval { scope name, ->{ where(approved: true) } }
+ end
+
+ assert_nothing_raised do
+ subklass.class_eval { scope name, ->{ where(approved: true) } }
+ end
+ end
+ end
+
# Method delegation for scope names which look like /\A[a-zA-Z_]\w*[!?]?\z/
# has been done by evaluating a string with a plain def statement. For scope
# names which contain spaces this approach doesn't work.
diff --git a/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md
index 95bf5601f2..5f9591ccb1 100644
--- a/activesupport/CHANGELOG.md
+++ b/activesupport/CHANGELOG.md
@@ -1,3 +1,10 @@
+* Remove the deprecation about the `#filter` method
+
+ Filter objects should now rely on method corresponding to the filter type
+ (e.g. `#before`)
+
+ *Aaron Patterson*
+
* Add `ActiveSupport::JSON::Encoding.time_precision` as a way to configure the
precision of encoded time values:
diff --git a/activesupport/lib/active_support/message_encryptor.rb b/activesupport/lib/active_support/message_encryptor.rb
index 7773611e11..b019ad0dec 100644
--- a/activesupport/lib/active_support/message_encryptor.rb
+++ b/activesupport/lib/active_support/message_encryptor.rb
@@ -12,7 +12,7 @@ module ActiveSupport
# This can be used in situations similar to the <tt>MessageVerifier</tt>, but
# where you don't want users to be able to determine the value of the payload.
#
- # salt = SecureRandom.random_bytes(64)
+ # salt = SecureRandom.random_bytes(64)
# key = ActiveSupport::KeyGenerator.new('password').generate_key(salt) # => "\x89\xE0\x156\xAC..."
# crypt = ActiveSupport::MessageEncryptor.new(key) # => #<ActiveSupport::MessageEncryptor ...>
# encrypted_data = crypt.encrypt_and_sign('my secret data') # => "NlFBTTMwOUV5UlA1QlNEN2xkY2d6eThYWWh..."
diff --git a/guides/source/4_1_release_notes.md b/guides/source/4_1_release_notes.md
index 477268f4bc..7399bfb5de 100644
--- a/guides/source/4_1_release_notes.md
+++ b/guides/source/4_1_release_notes.md
@@ -567,6 +567,9 @@ for detailed changes.
* Removed deprecated `assert_present` and `assert_blank` methods, use `assert
object.blank?` and `assert object.present?` instead.
+* Remove deprecated `#filter` method for filter objects, use the corresponding
+ method instead (e.g. `#before` for a before filter).
+
### Deprecations
* Deprecated `Numeric#{ago,until,since,from_now}`, the user is expected to
diff --git a/guides/source/action_controller_overview.md b/guides/source/action_controller_overview.md
index f394daa6aa..6c82375ea1 100644
--- a/guides/source/action_controller_overview.md
+++ b/guides/source/action_controller_overview.md
@@ -112,6 +112,10 @@ NOTE: The actual URL in this example will be encoded as "/clients?ids%5b%5d=1&id
The value of `params[:ids]` will now be `["1", "2", "3"]`. Note that parameter values are always strings; Rails makes no attempt to guess or cast the type.
+NOTE: Values such as `[]`, `[nil]` or `[nil, nil, ...]` in `params` are replaced
+with `nil` for security reasons by default. See [Security Guide](security.html#unsafe-query-generation)
+for more information.
+
To send a hash you include the key name inside the brackets:
```html
@@ -377,6 +381,28 @@ You can also pass a `:domain` key and specify the domain name for the cookie:
YourApp::Application.config.session_store :cookie_store, key: '_your_app_session', domain: ".example.com"
```
+You can pass `:serializer` key to specify serializer for serializing session:
+
+```ruby
+YourApp::Application.config.session_store :cookie_store, key: '_your_app_session', serializer: :json_serializer
+```
+
+Default serializer is `:marshal_serializer`. When Symbol or String is passed it
+will look for appropriate class in `ActionDispatch::Session` namespace, so
+passing `:my_custom_serializer` would load
+`ActionDispatch::Session::MyCustomSerializer`.
+
+```ruby
+YourApp::Application.config.session_store :cookie_store, key: '_your_app_session', serializer: :my_custom_serializer
+```
+
+It is also possible to pass serializer object with defined `load` and `dump`
+public methods:
+
+```ruby
+YourApp::Application.config.session_store :cookie_store, key: '_your_app_session', serializer: MyCustomSerializer
+```
+
Rails sets up (for the CookieStore) a secret key used for signing the session data. This can be changed in `config/initializers/secret_token.rb`
```ruby
@@ -683,7 +709,7 @@ class ApplicationController < ActionController::Base
end
class LoginFilter
- def self.filter(controller)
+ def self.before(controller)
unless controller.send(:logged_in?)
controller.flash[:error] = "You must be logged in to access this section"
controller.redirect_to controller.new_login_url
@@ -692,7 +718,7 @@ class LoginFilter
end
```
-Again, this is not an ideal example for this filter, because it's not run in the scope of the controller but gets the controller passed as an argument. The filter class has a class method `filter` which gets run before or after the action, depending on if it's a before or after filter. Classes used as around filters can also use the same `filter` method, which will get run in the same way. The method must `yield` to execute the action. Alternatively, it can have both a `before` and an `after` method that are run before and after the action.
+Again, this is not an ideal example for this filter, because it's not run in the scope of the controller but gets the controller passed as an argument. The filter class must implement a method with the same name as the filter, so for the `before_action` filter the class must implement a `before` method, and so on. The `around` method must `yield` to execute the action.
Request Forgery Protection
--------------------------
diff --git a/guides/source/configuring.md b/guides/source/configuring.md
index 38f7162fcf..5e0010725e 100644
--- a/guides/source/configuring.md
+++ b/guides/source/configuring.md
@@ -352,6 +352,10 @@ value. Defaults to `'encrypted cookie'`.
* `config.action_dispatch.encrypted_signed_cookie_salt` sets the signed
encrypted cookies salt value. Defaults to `'signed encrypted cookie'`.
+* `config.action_dispatch.perform_deep_munge` configures whether `deep_munge`
+ method should be performed on the parameters. See [Security Guide](security.html#unsafe-query-generation)
+ for more information. It defaults to true.
+
* `ActionDispatch::Callbacks.before` takes a block of code to run before the request.
* `ActionDispatch::Callbacks.to_prepare` takes a block to run after `ActionDispatch::Callbacks.before`, but before the request. Runs for every request in `development` mode, but only once for `production` or environments with `cache_classes` set to `true`.
diff --git a/guides/source/security.md b/guides/source/security.md
index cffe7c85f1..70fb066b64 100644
--- a/guides/source/security.md
+++ b/guides/source/security.md
@@ -915,6 +915,49 @@ Content-Type: text/html
Under certain circumstances this would present the malicious HTML to the victim. However, this only seems to work with Keep-Alive connections (and many browsers are using one-time connections). But you can't rely on this. _In any case this is a serious bug, and you should update your Rails to version 2.0.5 or 2.1.2 to eliminate Header Injection (and thus response splitting) risks._
+Unsafe Query Generation
+-----------------------
+
+Due to the way Active Record interprets parameters in combination with the way
+that Rack parses query parameters it was possible to issue unexpected database
+queries with `IS NULL` where clauses. As a response to that security issue
+([CVE-2012-2660](https://groups.google.com/forum/#!searchin/rubyonrails-security/deep_munge/rubyonrails-security/8SA-M3as7A8/Mr9fi9X4kNgJ),
+[CVE-2012-2694](https://groups.google.com/forum/#!searchin/rubyonrails-security/deep_munge/rubyonrails-security/jILZ34tAHF4/7x0hLH-o0-IJ)
+and [CVE-2013-0155](https://groups.google.com/forum/#!searchin/rubyonrails-security/CVE-2012-2660/rubyonrails-security/c7jT-EeN9eI/L0u4e87zYGMJ))
+`deep_munge` method was introduced as a solution to keep Rails secure by default.
+
+Example of vulnerable code that could be used by attacker, if `deep_munge`
+wasn't performed is:
+
+```ruby
+unless params[:token].nil?
+ user = User.find_by_token(params[:token])
+ user.reset_password!
+end
+```
+
+When `params[:token]` is one of: `[]`, `[nil]`, `[nil, nil, ...]` or
+`['foo', nil]` it will bypass the test for `nil`, but `IS NULL` or
+`IN ('foo', NULL)` where clauses still will be added to the SQL query.
+
+To keep rails secure by default, `deep_munge` replaces some of the values with
+`nil`. Below table shows what the parameters look like based on `JSON` sent in
+request:
+
+| JSON | Parameters |
+|-----------------------------------|--------------------------|
+| `{ "person": null }` | `{ :person => nil }` |
+| `{ "person": [] }` | `{ :person => nil }` |
+| `{ "person": [null] }` | `{ :person => nil }` |
+| `{ "person": [null, null, ...] }` | `{ :person => nil }` |
+| `{ "person": ["foo", null] }` | `{ :person => ["foo"] }` |
+
+It is possible to return to old behaviour and disable `deep_munge` configuring
+your application if you are aware of the risk and know how to handle it:
+
+```ruby
+config.action_dispatch.perform_deep_munge = false
+```
Default Headers
---------------
diff --git a/railties/lib/rails/application.rb b/railties/lib/rails/application.rb
index 05acd78d98..36432e56ba 100644
--- a/railties/lib/rails/application.rb
+++ b/railties/lib/rails/application.rb
@@ -205,7 +205,8 @@ module Rails
"action_dispatch.http_auth_salt" => config.action_dispatch.http_auth_salt,
"action_dispatch.signed_cookie_salt" => config.action_dispatch.signed_cookie_salt,
"action_dispatch.encrypted_cookie_salt" => config.action_dispatch.encrypted_cookie_salt,
- "action_dispatch.encrypted_signed_cookie_salt" => config.action_dispatch.encrypted_signed_cookie_salt
+ "action_dispatch.encrypted_signed_cookie_salt" => config.action_dispatch.encrypted_signed_cookie_salt,
+ "action_dispatch.session_serializer" => config.session_options[:serializer]
})
end
end
diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/session_store.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/initializers/session_store.rb.tt
index 2bb9b82c61..923d423287 100644
--- a/railties/lib/rails/generators/rails/app/templates/config/initializers/session_store.rb.tt
+++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/session_store.rb.tt
@@ -1,3 +1,3 @@
# Be sure to restart your server when you modify this file.
-Rails.application.config.session_store :cookie_store, key: <%= "'_#{app_name}_session'" %>
+Rails.application.config.session_store :cookie_store, key: <%= "'_#{app_name}_session'" %>, serializer: :json_serializer
diff --git a/railties/test/generators/app_generator_test.rb b/railties/test/generators/app_generator_test.rb
index ddecee2ca1..8aa306c8e0 100644
--- a/railties/test/generators/app_generator_test.rb
+++ b/railties/test/generators/app_generator_test.rb
@@ -433,7 +433,7 @@ class AppGeneratorTest < Rails::Generators::TestCase
def test_new_hash_style
run_generator [destination_root]
assert_file "config/initializers/session_store.rb" do |file|
- assert_match(/config.session_store :cookie_store, key: '_.+_session'/, file)
+ assert_match(/config.session_store :cookie_store, key: '_.+_session', serializer: :json_serializer/, file)
end
end