strong_parameters 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,20 @@
1
+ Copyright 2012 David Heinemeier Hansson
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,44 @@
1
+ = Strong Parameters
2
+
3
+ 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.
4
+
5
+ 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.
6
+
7
+ class PeopleController < ActionController::Base
8
+ # This will raise an ActiveModel::ForbiddenAttributes exception because it's using mass assignment
9
+ # without an explicit permit step.
10
+ def create
11
+ Person.create(params[:person])
12
+ end
13
+
14
+ # This will pass with flying colors as long as there's a person key in the parameters, otherwise
15
+ # it'll raise a ActionController::MissingParameter exception, which will get caught by
16
+ # ActionController::Base and turned into that 400 Bad Request reply.
17
+ def update
18
+ redirect_to current_account.people.find(params[:id]).tap do |person|
19
+ person.update_attributes!(person_params)
20
+ end
21
+ end
22
+
23
+ private
24
+ # Using a private method to encapsulate the permissible parameters is just a good pattern
25
+ # since you'll be able to reuse the same permit list between create and update. Also, you
26
+ # can specialize this method with per-user checking of permissible attributes.
27
+ def person_params
28
+ params.required(:person).permit(:name, :age)
29
+ end
30
+ end
31
+
32
+ Thanks to Nick Kallen for the permit idea!
33
+
34
+ == Todos
35
+
36
+ * Make this play nice with nested parameters [???]. Design:
37
+
38
+ params.permit(:name, friends: [ :name, { family: [ :name ] }])
39
+
40
+ * Automatically permit parameters coming from a signed form [Yehuda]
41
+
42
+ == Compatibility
43
+
44
+ Due to a testing issue, this plugin is only fully compatible with rails/3-2-stable rev 275ee0dc7b and forward as well as rails/master rev b49a7ddce1 and forward.
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env rake
2
+ begin
3
+ require 'bundler/setup'
4
+ rescue LoadError
5
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
6
+ end
7
+ begin
8
+ require 'rdoc/task'
9
+ rescue LoadError
10
+ require 'rdoc/rdoc'
11
+ require 'rake/rdoctask'
12
+ RDoc::Task = Rake::RDocTask
13
+ end
14
+
15
+ RDoc::Task.new(:rdoc) do |rdoc|
16
+ rdoc.rdoc_dir = 'rdoc'
17
+ rdoc.title = 'StrongParameters'
18
+ rdoc.options << '--line-numbers'
19
+ rdoc.rdoc_files.include('README.rdoc')
20
+ rdoc.rdoc_files.include('lib/**/*.rb')
21
+ end
22
+
23
+
24
+
25
+
26
+ Bundler::GemHelper.install_tasks
27
+
28
+ require 'rake/testtask'
29
+
30
+ Rake::TestTask.new(:test) do |t|
31
+ t.libs << 'lib'
32
+ t.libs << 'test'
33
+ t.pattern = 'test/**/*_test.rb'
34
+ t.verbose = false
35
+ end
36
+
37
+
38
+ task :default => :test
@@ -0,0 +1,81 @@
1
+ require 'active_support/concern'
2
+ require 'active_support/core_ext/hash/indifferent_access'
3
+ require 'action_controller'
4
+
5
+ module ActionController
6
+ class ParameterMissing < IndexError
7
+ attr_reader :param
8
+
9
+ def initialize(param)
10
+ @param = param
11
+ super("key not found: #{param}")
12
+ end
13
+ end
14
+
15
+ class Parameters < ActiveSupport::HashWithIndifferentAccess
16
+ attr_accessor :permitted
17
+ alias :permitted? :permitted
18
+
19
+ def initialize(attributes = nil)
20
+ super(attributes)
21
+ @permitted = false
22
+ end
23
+
24
+ def permit!
25
+ @permitted = true
26
+ self
27
+ end
28
+
29
+ def required(key)
30
+ self[key].presence || raise(ActionController::ParameterMissing.new(key))
31
+ end
32
+
33
+ def permit(*keys)
34
+ slice(*keys).permit!
35
+ end
36
+
37
+ def [](key)
38
+ convert_hashes_to_parameters(key, super)
39
+ end
40
+
41
+ def fetch(key)
42
+ unless block_given? || key?(key)
43
+ raise ActionController::ParameterMissing.new(key)
44
+ end
45
+
46
+ convert_hashes_to_parameters(key, super)
47
+ end
48
+
49
+ def slice(*keys)
50
+ self.class.new(super)
51
+ end
52
+
53
+ private
54
+ def convert_hashes_to_parameters(key, value)
55
+ if value.is_a?(Parameters) || !value.is_a?(Hash)
56
+ value
57
+ else
58
+ # Convert to Parameters on first access
59
+ self[key] = self.class.new(value)
60
+ end
61
+ end
62
+ end
63
+
64
+ module StrongParameters
65
+ extend ActiveSupport::Concern
66
+
67
+ included do
68
+ rescue_from(ActionController::ParameterMissing) { head :bad_request }
69
+ end
70
+
71
+ def params
72
+ @_params ||= Parameters.new(request.parameters)
73
+ end
74
+
75
+ def params=(val)
76
+ @_params = val.is_a?(Hash) ? Parameters.new(val) : val
77
+ end
78
+ end
79
+ end
80
+
81
+ ActionController::Base.send :include, ActionController::StrongParameters
@@ -0,0 +1,16 @@
1
+ module ActiveModel
2
+ class ForbiddenAttributes < StandardError
3
+ end
4
+
5
+ module ForbiddenAttributesProtection
6
+ def sanitize_for_mass_assignment(new_attributes, options = {})
7
+ if !new_attributes.respond_to?(:permitted?) || (new_attributes.respond_to?(:permitted?) && new_attributes.permitted?)
8
+ super
9
+ else
10
+ raise ActiveModel::ForbiddenAttributes
11
+ end
12
+ end
13
+ end
14
+ end
15
+
16
+ ActiveModel.autoload :ForbiddenAttributesProtection
@@ -0,0 +1,2 @@
1
+ require 'action_controller/parameters'
2
+ require 'active_model/forbidden_attributes_protection'
@@ -0,0 +1,3 @@
1
+ module StrongParameters
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,25 @@
1
+ require 'test_helper'
2
+
3
+ class BooksController < ActionController::Base
4
+ def create
5
+ params.required(:book).required(:name)
6
+ head :ok
7
+ end
8
+ end
9
+
10
+ class ActionControllerRequiredParamsTest < ActionController::TestCase
11
+ tests BooksController
12
+
13
+ test "missing required parameters will raise exception" do
14
+ post :create, { magazine: { name: "Mjallo!" } }
15
+ assert_response :bad_request
16
+
17
+ post :create, { book: { title: "Mjallo!" } }
18
+ assert_response :bad_request
19
+ end
20
+
21
+ test "required parameters that are present will not raise" do
22
+ post :create, { book: { name: "Mjallo!" } }
23
+ assert_response :ok
24
+ end
25
+ end
@@ -0,0 +1,25 @@
1
+ require 'test_helper'
2
+
3
+ class PeopleController < ActionController::Base
4
+ def create
5
+ render text: params[:person].permitted? ? "untainted" : "tainted"
6
+ end
7
+
8
+ def create_with_permit
9
+ render text: params[:person].permit(:name).permitted? ? "untainted" : "tainted"
10
+ end
11
+ end
12
+
13
+ class ActionControllerTaintedParamsTest < ActionController::TestCase
14
+ tests PeopleController
15
+
16
+ test "parameters are tainted" do
17
+ post :create, { person: { name: "Mjallo!" } }
18
+ assert_equal "tainted", response.body
19
+ end
20
+
21
+ test "parameters can be permitted and are then not tainted" do
22
+ post :create_with_permit, { person: { name: "Mjallo!" } }
23
+ assert_equal "untainted", response.body
24
+ end
25
+ end
@@ -0,0 +1,30 @@
1
+ require 'test_helper'
2
+
3
+ class Person
4
+ include ActiveModel::MassAssignmentSecurity
5
+ include ActiveModel::ForbiddenAttributesProtection
6
+
7
+ public :sanitize_for_mass_assignment
8
+ end
9
+
10
+ class ActiveModelMassUpdateProtectionTest < ActiveSupport::TestCase
11
+ test "forbidden attributes cannot be used for mass updating" do
12
+ assert_raises(ActiveModel::ForbiddenAttributes) do
13
+ Person.new.sanitize_for_mass_assignment(ActionController::Parameters.new(a: "b"))
14
+ end
15
+ end
16
+
17
+ test "permitted attributes can be used for mass updating" do
18
+ assert_nothing_raised do
19
+ assert_equal({ "a" => "b" },
20
+ Person.new.sanitize_for_mass_assignment(ActionController::Parameters.new(a: "b").permit(:a)))
21
+ end
22
+ end
23
+
24
+ test "regular attributes should still be allowed" do
25
+ assert_nothing_raised do
26
+ assert_equal({ a: "b" },
27
+ Person.new.sanitize_for_mass_assignment(a: "b"))
28
+ end
29
+ end
30
+ end
File without changes
@@ -0,0 +1,88 @@
1
+  (0.2ms) begin transaction
2
+  (0.0ms) rollback transaction
3
+  (0.0ms) begin transaction
4
+  (0.0ms) rollback transaction
5
+  (0.0ms) begin transaction
6
+  (0.0ms) rollback transaction
7
+  (0.0ms) begin transaction
8
+  (0.0ms) rollback transaction
9
+  (0.0ms) begin transaction
10
+  (0.0ms) rollback transaction
11
+  (0.0ms) begin transaction
12
+  (0.0ms) rollback transaction
13
+  (0.2ms) begin transaction
14
+  (0.0ms) rollback transaction
15
+  (0.0ms) begin transaction
16
+  (0.0ms) rollback transaction
17
+  (0.0ms) begin transaction
18
+  (0.0ms) rollback transaction
19
+  (0.0ms) begin transaction
20
+  (0.0ms) rollback transaction
21
+  (0.0ms) begin transaction
22
+  (0.0ms) rollback transaction
23
+  (0.0ms) begin transaction
24
+  (0.0ms) rollback transaction
25
+  (0.2ms) begin transaction
26
+  (0.0ms) rollback transaction
27
+  (0.0ms) begin transaction
28
+  (0.0ms) rollback transaction
29
+  (0.0ms) begin transaction
30
+  (0.0ms) rollback transaction
31
+  (0.0ms) begin transaction
32
+  (0.0ms) rollback transaction
33
+  (0.0ms) begin transaction
34
+  (0.0ms) rollback transaction
35
+  (0.0ms) begin transaction
36
+  (0.0ms) rollback transaction
37
+  (0.2ms) begin transaction
38
+  (0.0ms) rollback transaction
39
+  (0.0ms) begin transaction
40
+  (0.0ms) rollback transaction
41
+  (0.0ms) begin transaction
42
+  (0.0ms) rollback transaction
43
+  (0.0ms) begin transaction
44
+  (0.0ms) rollback transaction
45
+  (0.0ms) begin transaction
46
+  (0.0ms) rollback transaction
47
+  (0.0ms) begin transaction
48
+  (0.0ms) rollback transaction
49
+  (0.2ms) begin transaction
50
+  (0.0ms) rollback transaction
51
+  (0.0ms) begin transaction
52
+  (0.0ms) rollback transaction
53
+  (0.0ms) begin transaction
54
+  (0.0ms) rollback transaction
55
+  (0.0ms) begin transaction
56
+  (0.0ms) rollback transaction
57
+  (0.0ms) begin transaction
58
+  (0.0ms) rollback transaction
59
+  (0.0ms) begin transaction
60
+  (0.0ms) rollback transaction
61
+  (0.2ms) begin transaction
62
+  (0.0ms) rollback transaction
63
+  (0.0ms) begin transaction
64
+  (0.0ms) rollback transaction
65
+  (0.0ms) begin transaction
66
+  (0.0ms) rollback transaction
67
+  (0.0ms) begin transaction
68
+  (0.0ms) rollback transaction
69
+  (0.0ms) begin transaction
70
+  (0.0ms) rollback transaction
71
+  (0.0ms) begin transaction
72
+  (0.0ms) rollback transaction
73
+  (0.2ms) begin transaction
74
+  (0.0ms) rollback transaction
75
+  (0.0ms) begin transaction
76
+  (0.0ms) rollback transaction
77
+  (0.0ms) begin transaction
78
+  (0.0ms) rollback transaction
79
+  (0.0ms) begin transaction
80
+  (0.0ms) rollback transaction
81
+  (0.0ms) begin transaction
82
+  (0.0ms) rollback transaction
83
+  (0.0ms) begin transaction
84
+  (0.0ms) rollback transaction
85
+  (0.3ms) begin transaction
86
+  (0.0ms) rollback transaction
87
+  (0.3ms) begin transaction
88
+  (0.0ms) rollback transaction
@@ -0,0 +1,10 @@
1
+ require 'test_helper'
2
+ require 'action_controller/parameters'
3
+
4
+ class ParametersRequireTest < ActiveSupport::TestCase
5
+ test "required parameters must be present not merely not nil" do
6
+ assert_raises(ActionController::ParameterMissing) do
7
+ ActionController::Parameters.new(person: {}).required(:person)
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,45 @@
1
+ require 'test_helper'
2
+ require 'action_controller/parameters'
3
+
4
+ class ParametersTaintTest < ActiveSupport::TestCase
5
+ setup do
6
+ @params = ActionController::Parameters.new({ person: {
7
+ age: "32", name: { first: "David", last: "Heinemeier Hansson" }
8
+ }})
9
+ end
10
+
11
+ test "fetch raises ParameterMissing exception" do
12
+ e = assert_raises(ActionController::ParameterMissing) do
13
+ @params.fetch :foo
14
+ end
15
+ assert_equal :foo, e.param
16
+ end
17
+
18
+ test "permitted is sticky on accessors" do
19
+ assert !@params.slice(:person).permitted?
20
+ assert !@params[:person][:name].permitted?
21
+
22
+ @params.each { |key, value| assert(value.permitted?) if key == :person }
23
+
24
+ assert !@params.fetch(:person).permitted?
25
+
26
+ assert !@params.values_at(:person).first.permitted?
27
+ end
28
+
29
+ test "permitted is sticky on mutators" do
30
+ assert !@params.delete_if { |k| k == :person }.permitted?
31
+ assert !@params.keep_if { |k,v| k == :person }.permitted?
32
+ end
33
+
34
+ test "permitted is sticky beyond merges" do
35
+ assert !@params.merge(a: "b").permitted?
36
+ end
37
+
38
+ test "modifying the parameters" do
39
+ @params[:person][:hometown] = "Chicago"
40
+ @params[:person][:family] = { brother: "Jonas" }
41
+
42
+ assert_equal "Chicago", @params[:person][:hometown]
43
+ assert_equal "Jonas", @params[:person][:family][:brother]
44
+ end
45
+ end
@@ -0,0 +1,27 @@
1
+ # Configure Rails Environment
2
+ ENV["RAILS_ENV"] = "test"
3
+
4
+ require 'test/unit'
5
+ require 'strong_parameters'
6
+
7
+ module ActionController
8
+ SharedTestRoutes = ActionDispatch::Routing::RouteSet.new
9
+ SharedTestRoutes.draw do
10
+ match ':controller(/:action)'
11
+ end
12
+
13
+ class Base
14
+ include ActionController::Testing
15
+ include SharedTestRoutes.url_helpers
16
+ end
17
+
18
+ class ActionController::TestCase
19
+ setup do
20
+ @routes = SharedTestRoutes
21
+ end
22
+ end
23
+ end
24
+
25
+
26
+ # Load support files
27
+ Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f }
metadata ADDED
@@ -0,0 +1,101 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: strong_parameters
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - David Heinemeier Hansson
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-03-21 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: actionpack
16
+ requirement: &70323933135920 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: 3.2.0
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *70323933135920
25
+ - !ruby/object:Gem::Dependency
26
+ name: activemodel
27
+ requirement: &70323933135420 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ! '>='
31
+ - !ruby/object:Gem::Version
32
+ version: 3.2.0
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: *70323933135420
36
+ - !ruby/object:Gem::Dependency
37
+ name: rake
38
+ requirement: &70323933135040 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ! '>='
42
+ - !ruby/object:Gem::Version
43
+ version: '0'
44
+ type: :development
45
+ prerelease: false
46
+ version_requirements: *70323933135040
47
+ description:
48
+ email:
49
+ - david@heinemeierhansson.com
50
+ executables: []
51
+ extensions: []
52
+ extra_rdoc_files: []
53
+ files:
54
+ - lib/action_controller/parameters.rb
55
+ - lib/active_model/forbidden_attributes_protection.rb
56
+ - lib/strong_parameters/version.rb
57
+ - lib/strong_parameters.rb
58
+ - MIT-LICENSE
59
+ - Rakefile
60
+ - README.rdoc
61
+ - test/action_controller_required_params_test.rb
62
+ - test/action_controller_tainted_params_test.rb
63
+ - test/active_model_mass_assignment_taint_protection_test.rb
64
+ - test/dummy/db/test.sqlite3
65
+ - test/dummy/log/test.log
66
+ - test/parameters_require_test.rb
67
+ - test/parameters_taint_test.rb
68
+ - test/test_helper.rb
69
+ homepage:
70
+ licenses: []
71
+ post_install_message:
72
+ rdoc_options: []
73
+ require_paths:
74
+ - lib
75
+ required_ruby_version: !ruby/object:Gem::Requirement
76
+ none: false
77
+ requirements:
78
+ - - ! '>='
79
+ - !ruby/object:Gem::Version
80
+ version: '0'
81
+ required_rubygems_version: !ruby/object:Gem::Requirement
82
+ none: false
83
+ requirements:
84
+ - - ! '>='
85
+ - !ruby/object:Gem::Version
86
+ version: '0'
87
+ requirements: []
88
+ rubyforge_project:
89
+ rubygems_version: 1.8.15
90
+ signing_key:
91
+ specification_version: 3
92
+ summary: Permitted and required parameters for Action Pack
93
+ test_files:
94
+ - test/action_controller_required_params_test.rb
95
+ - test/action_controller_tainted_params_test.rb
96
+ - test/active_model_mass_assignment_taint_protection_test.rb
97
+ - test/dummy/db/test.sqlite3
98
+ - test/dummy/log/test.log
99
+ - test/parameters_require_test.rb
100
+ - test/parameters_taint_test.rb
101
+ - test/test_helper.rb