From e86524c0c5a26ceec92895c830d1355ae47a7034 Mon Sep 17 00:00:00 2001 From: Xavier Noria Date: Tue, 8 Nov 2016 09:58:58 +0100 Subject: [PATCH] adds support for arbitrary hashes in strong parameters --- actionpack/CHANGELOG.md | 8 ++++ .../metal/strong_parameters.rb | 45 +++++++++++++++++++ .../parameters/parameters_permit_test.rb | 33 ++++++++++++++ guides/source/action_controller_overview.md | 18 ++++++-- 4 files changed, 101 insertions(+), 3 deletions(-) diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md index d4b8d9b456..b539c893e3 100644 --- a/actionpack/CHANGELOG.md +++ b/actionpack/CHANGELOG.md @@ -1,3 +1,11 @@ +* Add support for arbitrary hashes in strong parameters: + + ```ruby + params.permit(preferences: {}) + ``` + + *Xavier Noria* + * Add `ActionController::Parameters#merge!`, which behaves the same as `Hash#merge!`. *Yuji Yaginuma* diff --git a/actionpack/lib/action_controller/metal/strong_parameters.rb b/actionpack/lib/action_controller/metal/strong_parameters.rb index 98aacd53ab..fc9cec1e33 100644 --- a/actionpack/lib/action_controller/metal/strong_parameters.rb +++ b/actionpack/lib/action_controller/metal/strong_parameters.rb @@ -334,6 +334,15 @@ module ActionController # params = ActionController::Parameters.new(tags: ['rails', 'parameters']) # params.permit(tags: []) # + # Sometimes it is not possible or convenient to declare the valid keys of + # a hash parameter or its internal structure. Just map to an empty hash: + # + # params.permit(preferences: {}) + # + # but be careful because this opens the door to arbitrary input. In this + # case, +permit+ ensures values in the returned structure are permitted + # scalars and filters out anything else. + # # You can also use +permit+ on nested parameters, like: # # params = ActionController::Parameters.new({ @@ -766,6 +775,7 @@ module ActionController end EMPTY_ARRAY = [] + EMPTY_HASH = {} def hash_filter(params, filter) filter = filter.with_indifferent_access @@ -779,6 +789,11 @@ module ActionController array_of_permitted_scalars?(self[key]) do |val| params[key] = val end + elsif filter[key] == EMPTY_HASH + # Declaration { preferences: {} } + if value.is_a?(Parameters) + params[key] = permit_any_in_parameters(value) + end elsif non_scalar?(value) # Declaration { user: :name } or { user: [:name, :age, { address: ... }] }. params[key] = each_element(value) do |element| @@ -788,6 +803,36 @@ module ActionController end end + def permit_any_in_parameters(params) + self.class.new.tap do |sanitized| + params.each do |key, value| + if permitted_scalar?(value) + sanitized[key] = value + elsif value.is_a?(Array) + sanitized[key] = permit_any_in_array(value) + elsif value.is_a?(Parameters) + sanitized[key] = permit_any_in_parameters(value) + else + # Filter this one out. + end + end + end + end + + def permit_any_in_array(array) + [].tap do |sanitized| + array.each do |element| + if permitted_scalar?(element) + sanitized << element + elsif element.is_a?(Parameters) + sanitized << permit_any_in_parameters(element) + else + # Filter this one out. + end + end + end + end + def initialize_copy(source) super @parameters = @parameters.dup diff --git a/actionpack/test/controller/parameters/parameters_permit_test.rb b/actionpack/test/controller/parameters/parameters_permit_test.rb index 2b9a95d17c..16637d330b 100644 --- a/actionpack/test/controller/parameters/parameters_permit_test.rb +++ b/actionpack/test/controller/parameters/parameters_permit_test.rb @@ -168,6 +168,39 @@ class ParametersPermitTest < ActiveSupport::TestCase end end + test "key to empty hash: arbitrary hashes are permitted" do + params = ActionController::Parameters.new( + username: "fxn", + preferences: { + scheme: "Marazul", + font: { + name: "Source Code Pro", + size: 12 + }, + tabstops: [4, 8, 12, 16], + suspicious: [true, Object.new, false, /yo!/], + dubious: [{a: :a, b: /wtf!/}, {c: :c}], + injected: Object.new + }, + hacked: 1 # not a hash + ) + + permitted = params.permit(:username, preferences: {}, hacked: {}) + + assert_equal "fxn", permitted[:username] + assert_equal "Marazul", permitted[:preferences][:scheme] + assert_equal "Source Code Pro", permitted[:preferences][:font][:name] + assert_equal 12, permitted[:preferences][:font][:size] + assert_equal [4, 8, 12, 16], permitted[:preferences][:tabstops] + assert_equal [true, false], permitted[:preferences][:suspicious] + assert_equal :a, permitted[:preferences][:dubious][0][:a] + assert_equal :c, permitted[:preferences][:dubious][1][:c] + + assert_filtered_out permitted[:preferences][:dubious][0], :b + assert_filtered_out permitted[:preferences], :injected + assert_filtered_out permitted, :hacked + end + test "fetch raises ParameterMissing exception" do e = assert_raises(ActionController::ParameterMissing) do @params.fetch :foo diff --git a/guides/source/action_controller_overview.md b/guides/source/action_controller_overview.md index 7b1138c7d4..40eb838d32 100644 --- a/guides/source/action_controller_overview.md +++ b/guides/source/action_controller_overview.md @@ -258,6 +258,17 @@ scalar values, map the key to an empty array: params.permit(id: []) ``` +Sometimes it is not possible or convenient to declare the valid keys of +a hash parameter or its internal structure. Just map to an empty hash: + +```ruby +params.permit(preferences: {}) +``` + +but be careful because this opens the door to arbitrary input. In this +case, `permit` ensures values in the returned structure are permitted +scalars and filters out anything else. + To whitelist an entire hash of parameters, the `permit!` method can be used: @@ -265,9 +276,10 @@ used: params.require(:log_entry).permit! ``` -This will mark the `:log_entry` parameters hash and any sub-hash of it as -permitted. Extreme care should be taken when using `permit!`, as it -will allow all current and future model attributes to be mass-assigned. +This marks the `:log_entry` parameters hash and any sub-hash of it as +permitted and does not check for permitted scalars, anything is accepted. +Extreme care should be taken when using `permit!`, as it will allow all current +and future model attributes to be mass-assigned. #### Nested Parameters