Skip to content
This repository
Browse code

Mass assignment security refactoring

Signed-off-by: José Valim <jose.valim@gmail.com>
  • Loading branch information...
commit 606088df3f10dd8daec8ccc97d8279c119a503b5 1 parent 723a0bb
authored January 29, 2010 josevalim committed July 08, 2010
1  activerecord/lib/active_record.rb
@@ -64,6 +64,7 @@ module ActiveRecord
64 64
     autoload :CounterCache
65 65
     autoload :DynamicFinderMatch
66 66
     autoload :DynamicScopeMatch
  67
+    autoload :MassAssignmentSecurity
67 68
     autoload :Migration
68 69
     autoload :Migrator, 'active_record/migration'
69 70
     autoload :NamedScope
144  activerecord/lib/active_record/base.rb
@@ -24,7 +24,7 @@
24 24
 require 'active_record/log_subscriber'
25 25
 
26 26
 module ActiveRecord #:nodoc:
27  
-  # = Active Record 
  27
+  # = Active Record
28 28
   #
29 29
   # Active Record objects don't specify their attributes directly, but rather infer them from the table definition with
30 30
   # which they're linked. Adding, removing, and changing attributes and their type is done directly in the database. Any change
@@ -476,112 +476,16 @@ def count_by_sql(sql)
476 476
         connection.select_value(sql, "#{name} Count").to_i
477 477
       end
478 478
 
479  
-      # Attributes named in this macro are protected from mass-assignment,
480  
-      # such as <tt>new(attributes)</tt>,
481  
-      # <tt>update_attributes(attributes)</tt>, or
482  
-      # <tt>attributes=(attributes)</tt>.
483  
-      #
484  
-      # Mass-assignment to these attributes will simply be ignored, to assign
485  
-      # to them you can use direct writer methods. This is meant to protect
486  
-      # sensitive attributes from being overwritten by malicious users
487  
-      # tampering with URLs or forms.
488  
-      #
489  
-      #   class Customer < ActiveRecord::Base
490  
-      #     attr_protected :credit_rating
491  
-      #   end
492  
-      #
493  
-      #   customer = Customer.new("name" => David, "credit_rating" => "Excellent")
494  
-      #   customer.credit_rating # => nil
495  
-      #   customer.attributes = { "description" => "Jolly fellow", "credit_rating" => "Superb" }
496  
-      #   customer.credit_rating # => nil
497  
-      #
498  
-      #   customer.credit_rating = "Average"
499  
-      #   customer.credit_rating # => "Average"
500  
-      #
501  
-      # To start from an all-closed default and enable attributes as needed,
502  
-      # have a look at +attr_accessible+.
503  
-      #
504  
-      # If the access logic of your application is richer you can use <tt>Hash#except</tt>
505  
-      # or <tt>Hash#slice</tt> to sanitize the hash of parameters before they are
506  
-      # passed to Active Record.
507  
-      #
508  
-      # For example, it could be the case that the list of protected attributes
509  
-      # for a given model depends on the role of the user:
510  
-      #
511  
-      #   # Assumes plan_id is not protected because it depends on the role.
512  
-      #   params[:account] = params[:account].except(:plan_id) unless admin?
513  
-      #   @account.update_attributes(params[:account])
514  
-      #
515  
-      # Note that +attr_protected+ is still applied to the received hash. Thus,
516  
-      # with this technique you can at most _extend_ the list of protected
517  
-      # attributes for a particular mass-assignment call.
518  
-      def attr_protected(*attributes)
519  
-        write_inheritable_attribute(:attr_protected, Set.new(attributes.map {|a| a.to_s}) + (protected_attributes || []))
520  
-      end
521  
-
522  
-      # Returns an array of all the attributes that have been protected from mass-assignment.
523  
-      def protected_attributes # :nodoc:
524  
-        read_inheritable_attribute(:attr_protected)
525  
-      end
526  
-
527  
-      # Specifies a white list of model attributes that can be set via
528  
-      # mass-assignment, such as <tt>new(attributes)</tt>,
529  
-      # <tt>update_attributes(attributes)</tt>, or
530  
-      # <tt>attributes=(attributes)</tt>
531  
-      #
532  
-      # This is the opposite of the +attr_protected+ macro: Mass-assignment
533  
-      # will only set attributes in this list, to assign to the rest of
534  
-      # attributes you can use direct writer methods. This is meant to protect
535  
-      # sensitive attributes from being overwritten by malicious users
536  
-      # tampering with URLs or forms. If you'd rather start from an all-open
537  
-      # default and restrict attributes as needed, have a look at
538  
-      # +attr_protected+.
539  
-      #
540  
-      #   class Customer < ActiveRecord::Base
541  
-      #     attr_accessible :name, :nickname
542  
-      #   end
543  
-      #
544  
-      #   customer = Customer.new(:name => "David", :nickname => "Dave", :credit_rating => "Excellent")
545  
-      #   customer.credit_rating # => nil
546  
-      #   customer.attributes = { :name => "Jolly fellow", :credit_rating => "Superb" }
547  
-      #   customer.credit_rating # => nil
548  
-      #
549  
-      #   customer.credit_rating = "Average"
550  
-      #   customer.credit_rating # => "Average"
551  
-      #
552  
-      # If the access logic of your application is richer you can use <tt>Hash#except</tt>
553  
-      # or <tt>Hash#slice</tt> to sanitize the hash of parameters before they are
554  
-      # passed to Active Record.
555  
-      #
556  
-      # For example, it could be the case that the list of accessible attributes
557  
-      # for a given model depends on the role of the user:
558  
-      #
559  
-      #   # Assumes plan_id is accessible because it depends on the role.
560  
-      #   params[:account] = params[:account].except(:plan_id) unless admin?
561  
-      #   @account.update_attributes(params[:account])
562  
-      #
563  
-      # Note that +attr_accessible+ is still applied to the received hash. Thus,
564  
-      # with this technique you can at most _narrow_ the list of accessible
565  
-      # attributes for a particular mass-assignment call.
566  
-      def attr_accessible(*attributes)
567  
-        write_inheritable_attribute(:attr_accessible, Set.new(attributes.map(&:to_s)) + (accessible_attributes || []))
  479
+      # Attributes listed as readonly can be set for a new record, but will be ignored in database updates afterwards.
  480
+      def attr_readonly(*attributes)
  481
+        write_inheritable_attribute(:attr_readonly, Set.new(attributes.map(&:to_s)) + (readonly_attributes || []))
568 482
       end
569 483
 
570  
-      # Returns an array of all the attributes that have been made accessible to mass-assignment.
571  
-      def accessible_attributes # :nodoc:
572  
-        read_inheritable_attribute(:attr_accessible)
  484
+      # Returns an array of all the attributes that have been specified as readonly.
  485
+      def readonly_attributes
  486
+        read_inheritable_attribute(:attr_readonly) || []
573 487
       end
574 488
 
575  
-       # Attributes listed as readonly can be set for a new record, but will be ignored in database updates afterwards.
576  
-       def attr_readonly(*attributes)
577  
-         write_inheritable_attribute(:attr_readonly, Set.new(attributes.map(&:to_s)) + (readonly_attributes || []))
578  
-       end
579  
-
580  
-       # Returns an array of all the attributes that have been specified as readonly.
581  
-       def readonly_attributes
582  
-         read_inheritable_attribute(:attr_readonly) || []
583  
-       end
584  
-
585 489
       # If you have an attribute that needs to be saved to the database as an object, and retrieved as the same object,
586 490
       # then specify the name of that attribute using this method and it will be handled automatically.
587 491
       # The serialization is done through YAML. If +class_name+ is specified, the serialized object must be of that
@@ -1716,27 +1620,6 @@ def ensure_proper_type
1716 1620
         end
1717 1621
       end
1718 1622
 
1719  
-      def remove_attributes_protected_from_mass_assignment(attributes)
1720  
-        safe_attributes =
1721  
-          if self.class.accessible_attributes.nil? && self.class.protected_attributes.nil?
1722  
-            attributes.reject { |key, value| attributes_protected_by_default.include?(key.gsub(/\(.+/, "")) }
1723  
-          elsif self.class.protected_attributes.nil?
1724  
-            attributes.reject { |key, value| !self.class.accessible_attributes.include?(key.gsub(/\(.+/, "")) || attributes_protected_by_default.include?(key.gsub(/\(.+/, "")) }
1725  
-          elsif self.class.accessible_attributes.nil?
1726  
-            attributes.reject { |key, value| self.class.protected_attributes.include?(key.gsub(/\(.+/,"")) || attributes_protected_by_default.include?(key.gsub(/\(.+/, "")) }
1727  
-          else
1728  
-            raise "Declare either attr_protected or attr_accessible for #{self.class}, but not both."
1729  
-          end
1730  
-
1731  
-        removed_attributes = attributes.keys - safe_attributes.keys
1732  
-
1733  
-        if removed_attributes.any?
1734  
-          log_protected_attribute_removal(removed_attributes)
1735  
-        end
1736  
-
1737  
-        safe_attributes
1738  
-      end
1739  
-
1740 1623
       # Removes attributes which have been marked as readonly.
1741 1624
       def remove_readonly_attributes(attributes)
1742 1625
         unless self.class.readonly_attributes.nil?
@@ -1746,16 +1629,10 @@ def remove_readonly_attributes(attributes)
1746 1629
         end
1747 1630
       end
1748 1631
 
1749  
-      def log_protected_attribute_removal(*attributes)
1750  
-        if logger
1751  
-          logger.debug "WARNING: Can't mass-assign these protected attributes: #{attributes.join(', ')}"
1752  
-        end
1753  
-      end
1754  
-
1755 1632
       # The primary key and inheritance column can never be set by mass-assignment for security reasons.
1756  
-      def attributes_protected_by_default
1757  
-        default = [ self.class.primary_key, self.class.inheritance_column ]
1758  
-        default << 'id' unless self.class.primary_key.eql? 'id'
  1633
+      def self.attributes_protected_by_default
  1634
+        default = [ primary_key, inheritance_column ]
  1635
+        default << 'id' unless primary_key.eql? 'id'
1759 1636
         default
1760 1637
       end
1761 1638
 
@@ -1920,6 +1797,7 @@ def object_from_yaml(string)
1920 1797
     include AttributeMethods::PrimaryKey
1921 1798
     include AttributeMethods::TimeZoneConversion
1922 1799
     include AttributeMethods::Dirty
  1800
+    extend MassAssignmentSecurity
1923 1801
     include Callbacks, ActiveModel::Observing, Timestamp
1924 1802
     include Associations, AssociationPreload, NamedScope
1925 1803
 
160  activerecord/lib/active_record/mass_assignment_security.rb
... ...
@@ -0,0 +1,160 @@
  1
+require 'active_record/mass_assignment_security/permission_set'
  2
+
  3
+module ActiveRecord
  4
+  module MassAssignmentSecurity
  5
+    # Mass assignment security provides an interface for protecting attributes
  6
+    # from end-user assignment. For more complex permissions, mass assignment security
  7
+    # may be handled outside the model by extending a non-ActiveRecord class,
  8
+    # such as a controller, with this behavior.
  9
+    #
  10
+    # For example, a logged in user may need to assign additional attributes depending
  11
+    # on their role:
  12
+    #
  13
+    # class AccountsController < ApplicationController
  14
+    #   extend ActiveRecord::MassAssignmentSecurity
  15
+    #
  16
+    #   attr_accessible :first_name, :last_name
  17
+    #
  18
+    #   def self.admin_accessible_attributes
  19
+    #     accessible_attributes + [ :plan_id ]
  20
+    #   end
  21
+    #
  22
+    #   def update
  23
+    #     ...
  24
+    #     @account.update_attributes(account_params)
  25
+    #     ...
  26
+    #   end
  27
+    #
  28
+    #   protected
  29
+    #
  30
+    #   def account_params
  31
+    #     remove_attributes_protected_from_mass_assignment(params[:account])
  32
+    #   end
  33
+    #
  34
+    #   def mass_assignment_authorizer
  35
+    #     admin ? admin_accessible_attributes : super
  36
+    #   end
  37
+    #
  38
+    # end
  39
+    #
  40
+    def self.extended(base)
  41
+      base.send(:include, InstanceMethods)
  42
+    end
  43
+
  44
+    module InstanceMethods
  45
+
  46
+      protected
  47
+
  48
+        def remove_attributes_protected_from_mass_assignment(attributes)
  49
+          mass_assignment_authorizer.sanitize(attributes)
  50
+        end
  51
+
  52
+        def mass_assignment_authorizer
  53
+          self.class.mass_assignment_authorizer
  54
+        end
  55
+
  56
+    end
  57
+
  58
+    # Attributes named in this macro are protected from mass-assignment,
  59
+    # such as <tt>new(attributes)</tt>,
  60
+    # <tt>update_attributes(attributes)</tt>, or
  61
+    # <tt>attributes=(attributes)</tt>.
  62
+    #
  63
+    # Mass-assignment to these attributes will simply be ignored, to assign
  64
+    # to them you can use direct writer methods. This is meant to protect
  65
+    # sensitive attributes from being overwritten by malicious users
  66
+    # tampering with URLs or forms.
  67
+    #
  68
+    #   class Customer < ActiveRecord::Base
  69
+    #     attr_protected :credit_rating
  70
+    #   end
  71
+    #
  72
+    #   customer = Customer.new("name" => David, "credit_rating" => "Excellent")
  73
+    #   customer.credit_rating # => nil
  74
+    #   customer.attributes = { "description" => "Jolly fellow", "credit_rating" => "Superb" }
  75
+    #   customer.credit_rating # => nil
  76
+    #
  77
+    #   customer.credit_rating = "Average"
  78
+    #   customer.credit_rating # => "Average"
  79
+    #
  80
+    # To start from an all-closed default and enable attributes as needed,
  81
+    # have a look at +attr_accessible+.
  82
+    #
  83
+    # Note that using <tt>Hash#except</tt> or <tt>Hash#slice</tt> in place of +attr_protected+
  84
+    # to sanitize attributes won't provide sufficient protection.
  85
+    def attr_protected(*keys)
  86
+      use_authorizer(:protected_attributes)
  87
+      protected_attributes.merge(keys)
  88
+    end
  89
+
  90
+    # Specifies a white list of model attributes that can be set via
  91
+    # mass-assignment, such as <tt>new(attributes)</tt>,
  92
+    # <tt>update_attributes(attributes)</tt>, or
  93
+    # <tt>attributes=(attributes)</tt>
  94
+    #
  95
+    # This is the opposite of the +attr_protected+ macro: Mass-assignment
  96
+    # will only set attributes in this list, to assign to the rest of
  97
+    # attributes you can use direct writer methods. This is meant to protect
  98
+    # sensitive attributes from being overwritten by malicious users
  99
+    # tampering with URLs or forms. If you'd rather start from an all-open
  100
+    # default and restrict attributes as needed, have a look at
  101
+    # +attr_protected+.
  102
+    #
  103
+    #   class Customer < ActiveRecord::Base
  104
+    #     attr_accessible :name, :nickname
  105
+    #   end
  106
+    #
  107
+    #   customer = Customer.new(:name => "David", :nickname => "Dave", :credit_rating => "Excellent")
  108
+    #   customer.credit_rating # => nil
  109
+    #   customer.attributes = { :name => "Jolly fellow", :credit_rating => "Superb" }
  110
+    #   customer.credit_rating # => nil
  111
+    #
  112
+    #   customer.credit_rating = "Average"
  113
+    #   customer.credit_rating # => "Average"
  114
+    #
  115
+    # Note that using <tt>Hash#except</tt> or <tt>Hash#slice</tt> in place of +attr_accessible+
  116
+    # to sanitize attributes won't provide sufficient protection.
  117
+    def attr_accessible(*keys)
  118
+      use_authorizer(:accessible_attributes)
  119
+      accessible_attributes.merge(keys)
  120
+    end
  121
+
  122
+    # Returns an array of all the attributes that have been protected from mass-assignment.
  123
+    def protected_attributes
  124
+      read_inheritable_attribute(:protected_attributes) || begin
  125
+        authorizer = BlackList.new
  126
+        authorizer += attributes_protected_by_default
  127
+        authorizer.logger = logger
  128
+        write_inheritable_attribute(:protected_attributes, authorizer)
  129
+      end
  130
+    end
  131
+
  132
+    # Returns an array of all the attributes that have been made accessible to mass-assignment.
  133
+    def accessible_attributes
  134
+      read_inheritable_attribute(:accessible_attributes) || begin
  135
+        authorizer = WhiteList.new
  136
+        authorizer.logger = logger
  137
+        write_inheritable_attribute(:accessible_attributes, authorizer)
  138
+      end
  139
+    end
  140
+
  141
+    def mass_assignment_authorizer
  142
+      protected_attributes
  143
+    end
  144
+
  145
+    private
  146
+
  147
+      # Sets the active authorizer, (attr_protected or attr_accessible). Subsequent calls
  148
+      # will raise an exception when using a different authorizer_id.
  149
+      def use_authorizer(authorizer_id) # :nodoc:
  150
+        if active_authorizer_id = read_inheritable_attribute(:active_authorizer_id)
  151
+          unless authorizer_id == active_authorizer_id
  152
+            raise("Already using #{active_authorizer_id}, cannot use #{authorizer_id}")
  153
+          end
  154
+        else
  155
+          write_inheritable_attribute(:active_authorizer_id, authorizer_id)
  156
+        end
  157
+      end
  158
+
  159
+  end
  160
+end
44  activerecord/lib/active_record/mass_assignment_security/permission_set.rb
... ...
@@ -0,0 +1,44 @@
  1
+require 'active_record/mass_assignment_security/sanitizer'
  2
+
  3
+module ActiveRecord
  4
+  module MassAssignmentSecurity
  5
+    class PermissionSet < Set
  6
+
  7
+      attr_accessor :logger
  8
+
  9
+      def merge(values)
  10
+        super(values.map(&:to_s))
  11
+      end
  12
+
  13
+      def include?(key)
  14
+        super(remove_multiparameter_id(key))
  15
+      end
  16
+
  17
+      protected
  18
+
  19
+        def remove_multiparameter_id(key)
  20
+          key.gsub(/\(.+/, '')
  21
+        end
  22
+
  23
+    end
  24
+
  25
+    class WhiteList < PermissionSet
  26
+      include Sanitizer
  27
+
  28
+      def deny?(key)
  29
+        !include?(key)
  30
+      end
  31
+
  32
+    end
  33
+
  34
+    class BlackList < PermissionSet
  35
+      include Sanitizer
  36
+
  37
+      def deny?(key)
  38
+        include?(key)
  39
+      end
  40
+
  41
+    end
  42
+
  43
+  end
  44
+end
27  activerecord/lib/active_record/mass_assignment_security/sanitizer.rb
... ...
@@ -0,0 +1,27 @@
  1
+module ActiveRecord
  2
+  module MassAssignmentSecurity
  3
+    module Sanitizer
  4
+
  5
+      # Returns all attributes not denied by the authorizer.
  6
+      def sanitize(attributes)
  7
+        sanitized_attributes = attributes.reject { |key, value| deny?(key) }
  8
+        debug_protected_attribute_removal(attributes, sanitized_attributes) if debug?
  9
+        sanitized_attributes
  10
+      end
  11
+
  12
+      protected
  13
+
  14
+        def debug_protected_attribute_removal(attributes, sanitized_attributes)
  15
+          removed_keys = attributes.keys - sanitized_attributes.keys
  16
+          if removed_keys.any?
  17
+            logger.debug "WARNING: Can't mass-assign protected attributes: #{removed_keys.join(', ')}"
  18
+          end
  19
+        end
  20
+
  21
+        def debug?
  22
+          logger.present?
  23
+        end
  24
+
  25
+    end
  26
+  end
  27
+end
26  activerecord/test/cases/base_test.rb
@@ -71,9 +71,8 @@ class Task < ActiveRecord::Base
71 71
   attr_protected :starting
72 72
 end
73 73
 
74  
-class TopicWithProtectedContentAndAccessibleAuthorName < ActiveRecord::Base
  74
+class TopicWithProtectedContent < ActiveRecord::Base
75 75
   self.table_name = 'topics'
76  
-  attr_accessible :author_name
77 76
   attr_protected  :content
78 77
 end
79 78
 
@@ -956,9 +955,9 @@ def test_update_attributes!
956 955
   end
957 956
 
958 957
   def test_mass_assignment_should_raise_exception_if_accessible_and_protected_attribute_writers_are_both_used
959  
-    topic = TopicWithProtectedContentAndAccessibleAuthorName.new
960  
-    assert_raise(RuntimeError) { topic.attributes = { "author_name" => "me" } }
961  
-    assert_raise(RuntimeError) { topic.attributes = { "content" => "stuff" } }
  958
+    assert_raise(RuntimeError) do
  959
+      TopicWithProtectedContent.attr_accessible :author_name
  960
+    end
962 961
   end
963 962
 
964 963
   def test_mass_assignment_protection
@@ -1021,19 +1020,20 @@ def test_mass_assignment_accessible
1021 1020
   end
1022 1021
 
1023 1022
   def test_mass_assignment_protection_inheritance
1024  
-    assert_nil LoosePerson.accessible_attributes
1025  
-    assert_equal Set.new([ 'credit_rating', 'administrator' ]), LoosePerson.protected_attributes
  1023
+    assert LoosePerson.accessible_attributes.blank?
  1024
+    assert_equal Set.new([ 'credit_rating', 'administrator', *LoosePerson.attributes_protected_by_default ]), LoosePerson.protected_attributes
1026 1025
 
1027  
-    assert_nil LooseDescendant.accessible_attributes
1028  
-    assert_equal Set.new([ 'credit_rating', 'administrator', 'phone_number' ]), LooseDescendant.protected_attributes
  1026
+    assert LooseDescendant.accessible_attributes.blank?
  1027
+    assert_equal Set.new([ 'credit_rating', 'administrator', 'phone_number', *LoosePerson.attributes_protected_by_default ]), LooseDescendant.protected_attributes
1029 1028
 
1030  
-    assert_nil LooseDescendantSecond.accessible_attributes
1031  
-    assert_equal Set.new([ 'credit_rating', 'administrator', 'phone_number', 'name' ]), LooseDescendantSecond.protected_attributes, 'Running attr_protected twice in one class should merge the protections'
  1029
+    assert LooseDescendantSecond.accessible_attributes.blank?
  1030
+    assert_equal Set.new([ 'credit_rating', 'administrator', 'phone_number', 'name', *LoosePerson.attributes_protected_by_default ]), LooseDescendantSecond.protected_attributes,
  1031
+      'Running attr_protected twice in one class should merge the protections'
1032 1032
 
1033  
-    assert_nil TightPerson.protected_attributes
  1033
+    assert (TightPerson.protected_attributes - TightPerson.attributes_protected_by_default).blank?
1034 1034
     assert_equal Set.new([ 'name', 'address' ]), TightPerson.accessible_attributes
1035 1035
 
1036  
-    assert_nil TightDescendant.protected_attributes
  1036
+    assert (TightDescendant.protected_attributes - TightDescendant.attributes_protected_by_default).blank?
1037 1037
     assert_equal Set.new([ 'name', 'address', 'phone_number' ]), TightDescendant.accessible_attributes
1038 1038
   end
1039 1039
 
28  activerecord/test/cases/mass_assignment_security/black_list_test.rb
... ...
@@ -0,0 +1,28 @@
  1
+require "cases/helper"
  2
+
  3
+class BlackListTest < ActiveRecord::TestCase
  4
+
  5
+  def setup
  6
+    @black_list   = ActiveRecord::MassAssignmentSecurity::BlackList.new
  7
+    @included_key = 'admin'
  8
+    @black_list  += [ @included_key ]
  9
+  end
  10
+
  11
+  test "deny? is true for included items" do
  12
+    assert_equal true, @black_list.deny?(@included_key)
  13
+  end
  14
+
  15
+  test "deny? is false for non-included items" do
  16
+    assert_equal false, @black_list.deny?('first_name')
  17
+  end
  18
+
  19
+  test "sanitize attributes" do
  20
+    original_attributes = { 'first_name' => 'allowed', 'admin' => 'denied', 'admin(1)' => 'denied' }
  21
+    attributes = @black_list.sanitize(original_attributes)
  22
+
  23
+    assert attributes.key?('first_name'), "Allowed key shouldn't be rejected"
  24
+    assert !attributes.key?('admin'),     "Denied key should be rejected"
  25
+    assert !attributes.key?('admin(1)'),  "Multi-parameter key should be detected"
  26
+  end
  27
+
  28
+end
30  activerecord/test/cases/mass_assignment_security/permission_set_test.rb
... ...
@@ -0,0 +1,30 @@
  1
+require "cases/helper"
  2
+
  3
+class PermissionSetTest < ActiveRecord::TestCase
  4
+
  5
+  def setup
  6
+    @permission_list = ActiveRecord::MassAssignmentSecurity::PermissionSet.new
  7
+  end
  8
+
  9
+  test "+ stringifies added collection values" do
  10
+    symbol_collection = [ :admin ]
  11
+    @permission_list += symbol_collection
  12
+
  13
+    assert @permission_list.include?('admin'), "did not add collection to #{@permission_list.inspect}}"
  14
+  end
  15
+
  16
+  test "include? normalizes multi-parameter keys" do
  17
+    multi_param_key = 'admin(1)'
  18
+    @permission_list += [ 'admin' ]
  19
+
  20
+    assert_equal true, @permission_list.include?(multi_param_key), "#{multi_param_key} not found in #{@permission_list.inspect}"
  21
+  end
  22
+
  23
+  test "include? normal keys" do
  24
+    normal_key = 'admin'
  25
+    @permission_list +=  [ normal_key ]
  26
+
  27
+    assert_equal true,  @permission_list.include?(normal_key), "#{normal_key} not found in #{@permission_list.inspect}"
  28
+  end
  29
+
  30
+end
36  activerecord/test/cases/mass_assignment_security/sanitizer_test.rb
... ...
@@ -0,0 +1,36 @@
  1
+require "cases/helper"
  2
+
  3
+class SanitizerTest < ActiveRecord::TestCase
  4
+
  5
+  class SanitizingAuthorizer
  6
+    include ActiveRecord::MassAssignmentSecurity::Sanitizer
  7
+
  8
+    attr_accessor :logger
  9
+
  10
+    def deny?(key)
  11
+       [ 'admin' ].include?(key)
  12
+    end
  13
+
  14
+  end
  15
+
  16
+  def setup
  17
+    @sanitizer = SanitizingAuthorizer.new
  18
+  end
  19
+
  20
+  test "sanitize attributes" do
  21
+    original_attributes = { 'first_name' => 'allowed', 'admin' => 'denied' }
  22
+    attributes = @sanitizer.sanitize(original_attributes)
  23
+
  24
+    assert attributes.key?('first_name'), "Allowed key shouldn't be rejected"
  25
+    assert !attributes.key?('admin'),     "Denied key should be rejected"
  26
+  end
  27
+
  28
+  test "debug mass assignment removal" do
  29
+    original_attributes = { 'first_name' => 'allowed', 'admin' => 'denied' }
  30
+    log = StringIO.new
  31
+    @sanitizer.logger = Logger.new(log)
  32
+    @sanitizer.sanitize(original_attributes)
  33
+    assert (log.string =~ /admin/), "Should log removed attributes: #{log.string}"
  34
+  end
  35
+
  36
+end
28  activerecord/test/cases/mass_assignment_security/white_list_test.rb
... ...
@@ -0,0 +1,28 @@
  1
+require "cases/helper"
  2
+
  3
+class WhiteListTest < ActiveRecord::TestCase
  4
+
  5
+  def setup
  6
+    @white_list   = ActiveRecord::MassAssignmentSecurity::WhiteList.new
  7
+    @included_key = 'first_name'
  8
+    @white_list  += [ @included_key ]
  9
+  end
  10
+
  11
+  test "deny? is false for included items" do
  12
+    assert_equal false, @white_list.deny?(@included_key)
  13
+  end
  14
+
  15
+  test "deny? is true for non-included items" do
  16
+    assert_equal true, @white_list.deny?('admin')
  17
+  end
  18
+
  19
+  test "sanitize attributes" do
  20
+    original_attributes = { 'first_name' => 'allowed', 'admin' => 'denied', 'admin(1)' => 'denied' }
  21
+    attributes = @white_list.sanitize(original_attributes)
  22
+
  23
+    assert attributes.key?('first_name'), "Allowed key shouldn't be rejected"
  24
+    assert !attributes.key?('admin'),     "Denied key should be rejected"
  25
+    assert !attributes.key?('admin(1)'),  "Multi-parameter key should be detected"
  26
+  end
  27
+
  28
+end

2 notes on commit 606088d

Norman Clarke

This commit caused some working code in my friendly_id plugin to fail; previously accessible_attributes returned nil if attr_accessible had not been invoked; now it always returns an instance of WhiteList no matter what. Is this the desired behavior going forward, or an oversight? If this is intentional it's not a problem to update my code, I'm just curious.

José Valim
Owner

It's indeed intentional.

Please sign in to comment.
Something went wrong with that request. Please try again.