diff --git a/.gitignore b/.gitignore index 31eef5d..1d30d77 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ test/dummy/log/*.log test/dummy/tmp/ test/dummy/.sass-cache tmp/ +.idea/ diff --git a/.ruby-gemset b/.ruby-gemset new file mode 100644 index 0000000..f7da562 --- /dev/null +++ b/.ruby-gemset @@ -0,0 +1 @@ +strong diff --git a/.ruby-version b/.ruby-version new file mode 100644 index 0000000..d427c89 --- /dev/null +++ b/.ruby-version @@ -0,0 +1 @@ +ruby-2.1.5 diff --git a/Gemfile.lock b/Gemfile.lock index 90937d8..e4d3757 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -30,6 +30,9 @@ GEM erubis (2.6.6) abstract (>= 1.0.0) i18n (0.5.3) + metaclass (0.0.4) + mocha (1.1.0) + metaclass (~> 0.0.1) rack (1.2.8) rack-mount (0.6.14) rack (>= 1.0.0) @@ -49,6 +52,7 @@ PLATFORMS ruby DEPENDENCIES + mocha rake rdoc (>= 4.0.0) strong_parameters! diff --git a/README.md b/README.md index 3a4c7c7..1aaaa58 100644 --- a/README.md +++ b/README.md @@ -1,142 +1,32 @@ -[![Travis CI](https://secure.travis-ci.org/rails/strong_parameters.png)](http://travis-ci.org/rails/strong_parameters) [![Gem Version](https://badge.fury.io/rb/strong_parameters.png)](http://badge.fury.io/rb/strong_parameters) -# Strong Parameters +# Indiegogo Modified Strong Parameters -With this plugin Action Controller parameters are forbidden to be used in Active Model mass assignments until they have been whitelisted. This means you'll have to make a conscious choice about which attributes to allow for mass updating and thus prevent accidentally exposing that which shouldn't be exposed. +This is a modified version of the [Strong Parameters] +gem. It differs from the original gem in these ways: -In addition, parameters can be marked as required and flow through a predefined raise/rescue flow to end up as a 400 Bad Request with no effort. +* This one is not all or nothing. You include ActionController::StrongParameters + explicitly in + specific controllers. This allows you to have some controllers + with strong parameters and others without. -``` ruby -class PeopleController < ActionController::Base - # This will raise an ActiveModel::ForbiddenAttributes exception because it's using mass assignment - # without an explicit permit step. - def create - Person.create(params[:person]) - end - - # This will pass with flying colors as long as there's a person key in the parameters, otherwise - # it'll raise an ActionController::MissingParameter exception, which will get caught by - # ActionController::Base and turned into that 400 Bad Request reply. - def update - person = current_account.people.find(params[:id]) - person.update_attributes!(person_params) - redirect_to person - end - - private - # Using a private method to encapsulate the permissible parameters is just a good pattern - # since you'll be able to reuse the same permit list between create and update. Also, you - # can specialize this method with per-user checking of permissible attributes. - def person_params - params.require(:person).permit(:name, :age) - end -end -``` - -## Permitted Scalar Values - -Given - -``` ruby -params.permit(:id) -``` - -the key `:id` will pass the whitelisting if it appears in `params` and it has a permitted scalar value associated. Otherwise the key is going to be filtered out, so arrays, hashes, or any other objects cannot be injected. - -The permitted scalar types are `String`, `Symbol`, `NilClass`, `Numeric`, `TrueClass`, `FalseClass`, `Date`, `Time`, `DateTime`, `StringIO`, `IO`, `ActionDispatch::Http::UploadedFile` and `Rack::Test::UploadedFile`. - -To declare that the value in `params` must be an array of permitted scalar values map the key to an empty array: - -``` ruby -params.permit(:id => []) -``` - -To whitelist an entire hash of parameters, the `permit!` method can be used - -``` ruby -params.require(:log_entry).permit! -``` - -This will mark the `:log_entry` parameters hash and any subhash of it permitted. Extreme care should be taken when using `permit!` as it will allow all current and future model attributes to be mass-assigned. - -## Nested Parameters - -You can also use permit on nested parameters, like: - -``` ruby -params.permit(:name, {:emails => []}, :friends => [ :name, { :family => [ :name ], :hobbies => [] }]) -``` - -This declaration whitelists the `name`, `emails` and `friends` attributes. It is expected that `emails` will be an array of permitted scalar values and that `friends` will be an array of resources with specific attributes : they should have a `name` attribute (any permitted scalar values allowed), a `hobbies` attribute as an array of permitted scalar values, and a `family` attribute which is restricted to having a `name` (any permitted scalar values allowed, too). - -Thanks to Nick Kallen for the permit idea! - -## Require Multiple Parameters +* Likewise, with standard strong parameters, the logging vs raise vs + do nothing setting for unpermitted parameters effects all controllers. + This version allows you to log some controllers while raising in others. + To log, include ActionController::LoggingParameters. -If you want to make sure that multiple keys are present in a params hash, you can call the method twice: +To enable logging rather than exceptions in a particular controller +just include ActionController::LoggingParameters after including +ActionController::StrongParameters, like so: -``` ruby -params.require(:token) -params.require(:post).permit(:title) -``` - -## Handling of Unpermitted Keys - -By default parameter keys that are not explicitly permitted will be logged in the development and test environment. In other environments these parameters will simply be filtered out and ignored. - -Additionally, this behaviour can be changed by changing the `config.action_controller.action_on_unpermitted_parameters` property in your environment files. If set to `:log` the unpermitted attributes will be logged, if set to `:raise` an exception will be raised. - -## Use Outside of Controllers - -While Strong Parameters will enforce permitted and required values in your application controllers, keep in mind -that you will need to sanitize untrusted data used for mass assignment when in use outside of controllers. - -For example, if you retrieve JSON data from a third party API call and pass the unchecked parsed result on to -`Model.create`, undesired mass assignments could take place. You can alleviate this risk by slicing the hash data, -or wrapping the data in a new instance of `ActionController::Parameters` and declaring permissions the same as -you would in a controller. For example: - -``` ruby -raw_parameters = { :email => "john@example.com", :name => "John", :admin => true } -parameters = ActionController::Parameters.new(raw_parameters) -user = User.create(parameters.permit(:name, :email)) -``` - -## More Examples - -Head over to the [Rails guide about Action Controller](http://guides.rubyonrails.org/action_controller_overview.html#more-examples). - -## Installation - -In Gemfile: +```ruby + class NewBooksController < ActionController::Base + include ActionController::StrongParameters + include ActionController::LoggingParameters -``` ruby -gem 'strong_parameters' -``` - -and then run `bundle`. To activate the strong parameters, you need to include this module in -every model you want protected. - -``` ruby -class Post < ActiveRecord::Base - include ActiveModel::ForbiddenAttributesProtection -end -``` - -Alternatively, you can protect all Active Record resources by default by creating an initializer and pasting the line: - -``` ruby -ActiveRecord::Base.send(:include, ActiveModel::ForbiddenAttributesProtection) -``` - -If you want to now disable the default whitelisting that occurs in Rails 3.2, change the `config.active_record.whitelist_attributes` property in your `config/application.rb`: - -``` ruby -config.active_record.whitelist_attributes = false + def create + params.permit(:book => [:pages]) + head :ok + end + end ``` -This will allow you to remove / not have to use `attr_accessible` and do mass assignment inside your code and tests. - -## Compatibility - -This plugin is only fully compatible with Rails versions 3.0, 3.1 and 3.2 but not 4.0+, as it is part of Rails Core in 4.0. -An unofficial Rails 2 version is [strong_parameters_rails2](https://github.com/grosser/strong_parameters/tree/rails2). +[Strong Parameters]: https://github.com/rails/strong_parameters diff --git a/lib/action_controller/logging_parameters.rb b/lib/action_controller/logging_parameters.rb new file mode 100644 index 0000000..1323a53 --- /dev/null +++ b/lib/action_controller/logging_parameters.rb @@ -0,0 +1,31 @@ +module ActionController + class DecoratesParameters + attr_reader :params + cattr_accessor :logger + + self.logger = ActionController::Base.logger + + methods_to_delegate = (ActionController::Parameters.new.methods - Object.new.methods - [:permit]) + [:require] + delegate *methods_to_delegate, :to => :params + + def initialize(params) + @params = params + end + + def permit(key) + params.permit(key) + rescue ActionController::UnpermittedParameters => e + DecoratesParameters.logger.warn(e.message) + end + end + + module LoggingParameters + def params + @_params ||= DecoratesParameters.new(Parameters.new(request.parameters)) + end + + def params=(val) + @_params = val.is_a?(Hash) ? DecoratesParameters.new(Parameters.new(val)) : val + end + end +end diff --git a/lib/action_controller/parameters.rb b/lib/action_controller/parameters.rb index 9dc3d7d..bbdcab6 100644 --- a/lib/action_controller/parameters.rb +++ b/lib/action_controller/parameters.rb @@ -265,4 +265,4 @@ def params=(val) end end -ActiveSupport.on_load(:action_controller) { include ActionController::StrongParameters } +# ActiveSupport.on_load(:action_controller) { include ActionController::StrongParameters } diff --git a/lib/strong_parameters.rb b/lib/strong_parameters.rb index c428a40..08c6671 100644 --- a/lib/strong_parameters.rb +++ b/lib/strong_parameters.rb @@ -1,4 +1,5 @@ require 'action_controller/parameters' +require 'action_controller/logging_parameters' require 'active_model/forbidden_attributes_protection' require 'strong_parameters/railtie' require 'strong_parameters/log_subscriber' diff --git a/strong_parameters.gemspec b/strong_parameters.gemspec index 134f633..569eff6 100644 --- a/strong_parameters.gemspec +++ b/strong_parameters.gemspec @@ -21,4 +21,5 @@ Gem::Specification.new do |s| s.add_dependency "railties", "~> 3.0" s.add_development_dependency "rake" + s.add_development_dependency "mocha" end diff --git a/test/action_controller_logging_parameters_test.rb b/test/action_controller_logging_parameters_test.rb new file mode 100644 index 0000000..41d1250 --- /dev/null +++ b/test/action_controller_logging_parameters_test.rb @@ -0,0 +1,45 @@ +require 'test_helper' + +class NewBooksController < ActionController::Base + include ActionController::StrongParameters + include ActionController::LoggingParameters + + def create + params.permit(:book => [:pages]) + head :ok + end +end + +class ActionControllerLoggingParametersTest < ActionController::TestCase + tests NewBooksController + + def setup + ActionController::Parameters.action_on_unpermitted_parameters = :log + end + + def teardown + ActionController::Parameters.action_on_unpermitted_parameters = false + end + + test "unpermitted parameters log and not raise" do + assert_logged('Unpermitted parameters: fishing') do + post :create, { :book => { :pages => 65 }, :fishing => "Turnips" } + end + assert_response :success + end + + def assert_logged(message) + old_logger = ActionController::Base.logger + log = StringIO.new + ActionController::Base.logger = Logger.new(log) + + begin + yield + + log.rewind + assert_match message, log.read + ensure + ActionController::Base.logger = old_logger + end + end +end diff --git a/test/action_controller_required_params_test.rb b/test/action_controller_required_params_test.rb index 5950f93..1fd6248 100644 --- a/test/action_controller_required_params_test.rb +++ b/test/action_controller_required_params_test.rb @@ -1,6 +1,8 @@ require 'test_helper' class BooksController < ActionController::Base + include ActionController::StrongParameters + def create params.require(:book).require(:name) head :ok diff --git a/test/action_controller_tainted_params_test.rb b/test/action_controller_tainted_params_test.rb index e2c6d8a..13e44c3 100644 --- a/test/action_controller_tainted_params_test.rb +++ b/test/action_controller_tainted_params_test.rb @@ -1,6 +1,8 @@ require 'test_helper' class PeopleController < ActionController::Base + include ActionController::StrongParameters + def create render :text => params[:person].permitted? ? "untainted" : "tainted" end diff --git a/test/decorates_params_actually_logs_on_unpermitted_params_test.rb b/test/decorates_params_actually_logs_on_unpermitted_params_test.rb new file mode 100644 index 0000000..702feca --- /dev/null +++ b/test/decorates_params_actually_logs_on_unpermitted_params_test.rb @@ -0,0 +1,46 @@ +require 'test_helper' +require 'action_controller/parameters' +require 'action_controller/logging_parameters' + +class DecoratesParamsActuallyLogsOnUnpermittedParamsTest < ActiveSupport::TestCase + def setup + ActionController::Parameters.action_on_unpermitted_parameters = :raise + end + + def teardown + ActionController::Parameters.action_on_unpermitted_parameters = false + end + + test "doesnt raise on unexpected params" do + params = ActionController::DecoratesParameters.new(ActionController::Parameters.new({ + :book => { :pages => 65 }, + :fishing => "Turnips" + })) + + assert_logged('found unpermitted parameters: fishing') do + params.permit(:book => [:pages]) + end + end + + test "doesnt raise on unexpected nested params" do + params = ActionController::DecoratesParameters.new(ActionController::Parameters.new({ + :book => { :pages => 65, :title => "Green Cats and where to find then." } + })) + + assert_logged('found unpermitted parameters: title') do + params.permit(:book => [:pages]) + end + end + + def assert_logged(message) + log = StringIO.new + ActionController::DecoratesParameters.logger = Logger.new(log) + + begin + yield + + log.rewind + assert_match message, log.read + end + end +end \ No newline at end of file