aboutsummaryrefslogtreecommitdiffstats
path: root/actionpack/lib/action_controller
diff options
context:
space:
mode:
Diffstat (limited to 'actionpack/lib/action_controller')
-rw-r--r--actionpack/lib/action_controller/metal/request_forgery_protection.rb7
-rw-r--r--actionpack/lib/action_controller/metal/strong_parameters.rb178
-rw-r--r--actionpack/lib/action_controller/railtie.rb19
-rw-r--r--actionpack/lib/action_controller/test_case.rb23
4 files changed, 190 insertions, 37 deletions
diff --git a/actionpack/lib/action_controller/metal/request_forgery_protection.rb b/actionpack/lib/action_controller/metal/request_forgery_protection.rb
index c5db0cb0d4..2d5ba0024e 100644
--- a/actionpack/lib/action_controller/metal/request_forgery_protection.rb
+++ b/actionpack/lib/action_controller/metal/request_forgery_protection.rb
@@ -102,15 +102,16 @@ module ActionController #:nodoc:
# This is the method that defines the application behavior when a request is found to be unverified.
def handle_unverified_request
- request.session = NullSessionHash.new
+ request.session = NullSessionHash.new(request.env)
request.env['action_dispatch.request.flash_hash'] = nil
request.env['rack.session.options'] = { skip: true }
request.env['action_dispatch.cookies'] = NullCookieJar.build(request)
end
class NullSessionHash < Rack::Session::Abstract::SessionHash #:nodoc:
- def initialize
- super(nil, nil)
+ def initialize(env)
+ super(nil, env)
+ @data = {}
@loaded = true
end
diff --git a/actionpack/lib/action_controller/metal/strong_parameters.rb b/actionpack/lib/action_controller/metal/strong_parameters.rb
index e6e58ce6cd..7e720ca6f5 100644
--- a/actionpack/lib/action_controller/metal/strong_parameters.rb
+++ b/actionpack/lib/action_controller/metal/strong_parameters.rb
@@ -1,6 +1,7 @@
require 'active_support/core_ext/hash/indifferent_access'
require 'active_support/core_ext/array/wrap'
require 'active_support/rescuable'
+require 'action_dispatch/http/upload'
module ActionController
# Raised when a required parameter is missing.
@@ -19,6 +20,20 @@ module ActionController
end
end
+ # Raised when a supplied parameter is not expected.
+ #
+ # params = ActionController::Parameters.new(a: "123", b: "456")
+ # params.permit(:c)
+ # # => ActionController::UnpermittedParameters: found unexpected keys: a, b
+ class UnpermittedParameters < IndexError
+ attr_reader :params # :nodoc:
+
+ def initialize(params) # :nodoc:
+ @params = params
+ super("found unpermitted parameters: #{params.join(", ")}")
+ end
+ end
+
# == Action Controller \Parameters
#
# Allows to choose which attributes should be whitelisted for mass updating
@@ -43,10 +58,15 @@ module ActionController
# Person.first.update!(permitted)
# # => #<Person id: 1, name: "Francesco", age: 22, role: "user">
#
- # It provides a +permit_all_parameters+ option that controls the top-level
- # behavior of new instances. If it's +true+, all the parameters will be
- # permitted by default. The default value for +permit_all_parameters+
- # option is +false+.
+ # It provides two options that controls the top-level behavior of new instances:
+ #
+ # * +permit_all_parameters+ - If it's +true+, all the parameters will be
+ # permitted by default. The default is +false+.
+ # * +action_on_unpermitted_parameters+ - Allow to control the behavior when parameters
+ # that are not explicitly permitted are found. The values can be <tt>:log</tt> to
+ # write a message on the logger or <tt>:raise</tt> to raise
+ # ActionController::UnpermittedParameters exception. The default value is <tt>:log</tt>
+ # in test and development environments, +false+ otherwise.
#
# params = ActionController::Parameters.new
# params.permitted? # => false
@@ -56,6 +76,16 @@ module ActionController
# params = ActionController::Parameters.new
# params.permitted? # => true
#
+ # params = ActionController::Parameters.new(a: "123", b: "456")
+ # params.permit(:c)
+ # # => {}
+ #
+ # ActionController::Parameters.action_on_unpermitted_parameters = :raise
+ #
+ # params = ActionController::Parameters.new(a: "123", b: "456")
+ # params.permit(:c)
+ # # => ActionController::UnpermittedParameters: found unpermitted keys: a, b
+ #
# <tt>ActionController::Parameters</tt> is inherited from
# <tt>ActiveSupport::HashWithIndifferentAccess</tt>, this means
# that you can fetch values using either <tt>:key</tt> or <tt>"key"</tt>.
@@ -65,6 +95,11 @@ module ActionController
# params["key"] # => "value"
class Parameters < ActiveSupport::HashWithIndifferentAccess
cattr_accessor :permit_all_parameters, instance_accessor: false
+ cattr_accessor :action_on_unpermitted_parameters, instance_accessor: false
+
+ # Never raise an UnpermittedParameters exception because of these params
+ # are present. They are added by Rails and it's of no concern.
+ NEVER_UNPERMITTED_PARAMS = %w( controller action )
# Returns a new instance of <tt>ActionController::Parameters</tt>.
# Also, sets the +permitted+ attribute to the default value of
@@ -150,6 +185,21 @@ module ActionController
# permitted.has_key?(:age) # => true
# permitted.has_key?(:role) # => false
#
+ # Only permitted scalars pass the filter. For example, given
+ #
+ # params.permit(:name)
+ #
+ # +:name+ passes it is a key of +params+ whose associated value is of type
+ # +String+, +Symbol+, +NilClass+, +Numeric+, +TrueClass+, +FalseClass+,
+ # +Date+, +Time+, +DateTime+, +StringIO+, +IO+, or
+ # +ActionDispatch::Http::UploadedFile+. Otherwise, the key +:name+ is
+ # filtered out.
+ #
+ # You may declare that the parameter should be an array of permitted scalars
+ # by mapping it to an empty array:
+ #
+ # params.permit(tags: [])
+ #
# You can also use +permit+ on nested parameters, like:
#
# params = ActionController::Parameters.new({
@@ -196,32 +246,15 @@ module ActionController
filters.flatten.each do |filter|
case filter
- when Symbol, String then
- if has_key?(filter)
- _value = self[filter]
- params[filter] = _value unless Hash === _value
- end
- keys.grep(/\A#{Regexp.escape(filter)}\(\d+[if]?\)\z/) { |key| params[key] = self[key] }
+ when Symbol, String
+ permitted_scalar_filter(params, filter)
when Hash then
- filter = filter.with_indifferent_access
-
- self.slice(*filter.keys).each do |key, values|
- return unless values
-
- key = key.to_sym
-
- params[key] = each_element(values) do |value|
- # filters are a Hash, so we expect value to be a Hash too
- next if filter.is_a?(Hash) && !value.is_a?(Hash)
-
- value = self.class.new(value) if !value.respond_to?(:permit)
-
- value.permit(*Array.wrap(filter[key]))
- end
- end
+ hash_filter(params, filter)
end
end
+ unpermitted_parameters!(params) if self.class.action_on_unpermitted_parameters
+
params.permit!
end
@@ -300,6 +333,99 @@ module ActionController
yield object
end
end
+
+ def unpermitted_parameters!(params)
+ unpermitted_keys = unpermitted_keys(params)
+ if unpermitted_keys.any?
+ case self.class.action_on_unpermitted_parameters
+ when :log
+ ActionController::Base.logger.debug "Unpermitted parameters: #{unpermitted_keys.join(", ")}"
+ when :raise
+ raise ActionController::UnpermittedParameters.new(unpermitted_keys)
+ end
+ end
+ end
+
+ def unpermitted_keys(params)
+ self.keys - params.keys - NEVER_UNPERMITTED_PARAMS
+ end
+
+ #
+ # --- Filtering ----------------------------------------------------------
+ #
+
+ # This is a white list of permitted scalar types that includes the ones
+ # supported in XML and JSON requests.
+ #
+ # This list is in particular used to filter ordinary requests, String goes
+ # as first element to quickly short-circuit the common case.
+ #
+ # If you modify this collection please update the API of +permit+ above.
+ PERMITTED_SCALAR_TYPES = [
+ String,
+ Symbol,
+ NilClass,
+ Numeric,
+ TrueClass,
+ FalseClass,
+ Date,
+ Time,
+ # DateTimes are Dates, we document the type but avoid the redundant check.
+ StringIO,
+ IO,
+ ActionDispatch::Http::UploadedFile,
+ ]
+
+ def permitted_scalar?(value)
+ PERMITTED_SCALAR_TYPES.any? {|type| value.is_a?(type)}
+ end
+
+ def permitted_scalar_filter(params, key)
+ if has_key?(key) && permitted_scalar?(self[key])
+ params[key] = self[key]
+ end
+
+ keys.grep(/\A#{Regexp.escape(key)}\(\d+[if]?\)\z/) do |k|
+ if permitted_scalar?(self[k])
+ params[k] = self[k]
+ end
+ end
+ end
+
+ def array_of_permitted_scalars?(value)
+ if value.is_a?(Array)
+ value.all? {|element| permitted_scalar?(element)}
+ end
+ end
+
+ def array_of_permitted_scalars_filter(params, key)
+ if has_key?(key) && array_of_permitted_scalars?(self[key])
+ params[key] = self[key]
+ end
+ end
+
+ EMPTY_ARRAY = []
+ def hash_filter(params, filter)
+ filter = filter.with_indifferent_access
+
+ # Slicing filters out non-declared keys.
+ slice(*filter.keys).each do |key, value|
+ return unless value
+
+ if filter[key] == EMPTY_ARRAY
+ # Declaration { comment_ids: [] }.
+ array_of_permitted_scalars_filter(params, key)
+ else
+ # Declaration { user: :name } or { user: [:name, :age, { adress: ... }] }.
+ params[key] = each_element(value) do |element|
+ if element.is_a?(Hash)
+ element = self.class.new(element) unless element.respond_to?(:permit)
+ element.permit(*Array.wrap(filter[key]))
+ end
+ end
+ end
+ end
+ end
end
# == Strong \Parameters
diff --git a/actionpack/lib/action_controller/railtie.rb b/actionpack/lib/action_controller/railtie.rb
index 3e44155f73..5379547c57 100644
--- a/actionpack/lib/action_controller/railtie.rb
+++ b/actionpack/lib/action_controller/railtie.rb
@@ -20,22 +20,27 @@ module ActionController
end
initializer "action_controller.parameters_config" do |app|
- ActionController::Parameters.permit_all_parameters = app.config.action_controller.delete(:permit_all_parameters) { false }
+ options = app.config.action_controller
+
+ ActionController::Parameters.permit_all_parameters = options.delete(:permit_all_parameters) { false }
+ ActionController::Parameters.action_on_unpermitted_parameters = options.delete(:action_on_unpermitted_parameters) do
+ (Rails.env.test? || Rails.env.development?) ? :log : false
+ end
end
initializer "action_controller.set_configs" do |app|
paths = app.config.paths
options = app.config.action_controller
- options.logger ||= Rails.logger
- options.cache_store ||= Rails.cache
+ options.logger ||= Rails.logger
+ options.cache_store ||= Rails.cache
- options.javascripts_dir ||= paths["public/javascripts"].first
- options.stylesheets_dir ||= paths["public/stylesheets"].first
+ options.javascripts_dir ||= paths["public/javascripts"].first
+ options.stylesheets_dir ||= paths["public/stylesheets"].first
# Ensure readers methods get compiled
- options.asset_host ||= app.config.asset_host
- options.relative_url_root ||= app.config.relative_url_root
+ options.asset_host ||= app.config.asset_host
+ options.relative_url_root ||= app.config.relative_url_root
ActiveSupport.on_load(:action_controller) do
include app.routes.mounted_helpers
diff --git a/actionpack/lib/action_controller/test_case.rb b/actionpack/lib/action_controller/test_case.rb
index d8206b573d..e9cf4372e4 100644
--- a/actionpack/lib/action_controller/test_case.rb
+++ b/actionpack/lib/action_controller/test_case.rb
@@ -235,18 +235,39 @@ module ActionController
end
end
+ # Methods #destroy and #load! are overridden to avoid calling methods on the
+ # @store object, which does not exist for the TestSession class.
class TestSession < Rack::Session::Abstract::SessionHash #:nodoc:
DEFAULT_OPTIONS = Rack::Session::Abstract::ID::DEFAULT_OPTIONS
def initialize(session = {})
super(nil, nil)
- replace(session.stringify_keys)
+ @id = SecureRandom.hex(16)
+ @data = stringify_keys(session)
@loaded = true
end
def exists?
true
end
+
+ def keys
+ @data.keys
+ end
+
+ def values
+ @data.values
+ end
+
+ def destroy
+ clear
+ end
+
+ private
+
+ def load!
+ @id
+ end
end
# Superclass for ActionController functional tests. Functional tests allow you to