-
Notifications
You must be signed in to change notification settings - Fork 21.4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Mass assignment security refactoring
Signed-off-by: José Valim <jose.valim@gmail.com>
- Loading branch information
Showing
10 changed files
with
378 additions
and
146 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
160 changes: 160 additions & 0 deletions
160
activerecord/lib/active_record/mass_assignment_security.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,160 @@ | ||
require 'active_record/mass_assignment_security/permission_set' | ||
|
||
module ActiveRecord | ||
module MassAssignmentSecurity | ||
# Mass assignment security provides an interface for protecting attributes | ||
# from end-user assignment. For more complex permissions, mass assignment security | ||
# may be handled outside the model by extending a non-ActiveRecord class, | ||
# such as a controller, with this behavior. | ||
# | ||
# For example, a logged in user may need to assign additional attributes depending | ||
# on their role: | ||
# | ||
# class AccountsController < ApplicationController | ||
# extend ActiveRecord::MassAssignmentSecurity | ||
# | ||
# attr_accessible :first_name, :last_name | ||
# | ||
# def self.admin_accessible_attributes | ||
# accessible_attributes + [ :plan_id ] | ||
# end | ||
# | ||
# def update | ||
# ... | ||
# @account.update_attributes(account_params) | ||
# ... | ||
# end | ||
# | ||
# protected | ||
# | ||
# def account_params | ||
# remove_attributes_protected_from_mass_assignment(params[:account]) | ||
# end | ||
# | ||
# def mass_assignment_authorizer | ||
# admin ? admin_accessible_attributes : super | ||
# end | ||
# | ||
# end | ||
# | ||
def self.extended(base) | ||
base.send(:include, InstanceMethods) | ||
end | ||
|
||
module InstanceMethods | ||
|
||
protected | ||
|
||
def remove_attributes_protected_from_mass_assignment(attributes) | ||
mass_assignment_authorizer.sanitize(attributes) | ||
end | ||
|
||
def mass_assignment_authorizer | ||
self.class.mass_assignment_authorizer | ||
end | ||
|
||
end | ||
|
||
# Attributes named in this macro are protected from mass-assignment, | ||
# such as <tt>new(attributes)</tt>, | ||
# <tt>update_attributes(attributes)</tt>, or | ||
# <tt>attributes=(attributes)</tt>. | ||
# | ||
# Mass-assignment to these attributes will simply be ignored, to assign | ||
# to them you can use direct writer methods. This is meant to protect | ||
# sensitive attributes from being overwritten by malicious users | ||
# tampering with URLs or forms. | ||
# | ||
# class Customer < ActiveRecord::Base | ||
# attr_protected :credit_rating | ||
# end | ||
# | ||
# customer = Customer.new("name" => David, "credit_rating" => "Excellent") | ||
# customer.credit_rating # => nil | ||
# customer.attributes = { "description" => "Jolly fellow", "credit_rating" => "Superb" } | ||
# customer.credit_rating # => nil | ||
# | ||
# customer.credit_rating = "Average" | ||
# customer.credit_rating # => "Average" | ||
# | ||
# To start from an all-closed default and enable attributes as needed, | ||
# have a look at +attr_accessible+. | ||
# | ||
# Note that using <tt>Hash#except</tt> or <tt>Hash#slice</tt> in place of +attr_protected+ | ||
# to sanitize attributes won't provide sufficient protection. | ||
def attr_protected(*keys) | ||
use_authorizer(:protected_attributes) | ||
protected_attributes.merge(keys) | ||
end | ||
|
||
# Specifies a white list of model attributes that can be set via | ||
# mass-assignment, such as <tt>new(attributes)</tt>, | ||
# <tt>update_attributes(attributes)</tt>, or | ||
# <tt>attributes=(attributes)</tt> | ||
# | ||
# This is the opposite of the +attr_protected+ macro: Mass-assignment | ||
# will only set attributes in this list, to assign to the rest of | ||
# attributes you can use direct writer methods. This is meant to protect | ||
# sensitive attributes from being overwritten by malicious users | ||
# tampering with URLs or forms. If you'd rather start from an all-open | ||
# default and restrict attributes as needed, have a look at | ||
# +attr_protected+. | ||
# | ||
# class Customer < ActiveRecord::Base | ||
# attr_accessible :name, :nickname | ||
# end | ||
# | ||
# customer = Customer.new(:name => "David", :nickname => "Dave", :credit_rating => "Excellent") | ||
# customer.credit_rating # => nil | ||
# customer.attributes = { :name => "Jolly fellow", :credit_rating => "Superb" } | ||
# customer.credit_rating # => nil | ||
# | ||
# customer.credit_rating = "Average" | ||
# customer.credit_rating # => "Average" | ||
# | ||
# Note that using <tt>Hash#except</tt> or <tt>Hash#slice</tt> in place of +attr_accessible+ | ||
# to sanitize attributes won't provide sufficient protection. | ||
def attr_accessible(*keys) | ||
use_authorizer(:accessible_attributes) | ||
accessible_attributes.merge(keys) | ||
end | ||
|
||
# Returns an array of all the attributes that have been protected from mass-assignment. | ||
def protected_attributes | ||
read_inheritable_attribute(:protected_attributes) || begin | ||
authorizer = BlackList.new | ||
authorizer += attributes_protected_by_default | ||
authorizer.logger = logger | ||
write_inheritable_attribute(:protected_attributes, authorizer) | ||
end | ||
end | ||
|
||
# Returns an array of all the attributes that have been made accessible to mass-assignment. | ||
def accessible_attributes | ||
read_inheritable_attribute(:accessible_attributes) || begin | ||
authorizer = WhiteList.new | ||
authorizer.logger = logger | ||
write_inheritable_attribute(:accessible_attributes, authorizer) | ||
end | ||
end | ||
|
||
def mass_assignment_authorizer | ||
protected_attributes | ||
end | ||
|
||
private | ||
|
||
# Sets the active authorizer, (attr_protected or attr_accessible). Subsequent calls | ||
# will raise an exception when using a different authorizer_id. | ||
def use_authorizer(authorizer_id) # :nodoc: | ||
if active_authorizer_id = read_inheritable_attribute(:active_authorizer_id) | ||
unless authorizer_id == active_authorizer_id | ||
raise("Already using #{active_authorizer_id}, cannot use #{authorizer_id}") | ||
end | ||
else | ||
write_inheritable_attribute(:active_authorizer_id, authorizer_id) | ||
end | ||
end | ||
|
||
end | ||
end |
44 changes: 44 additions & 0 deletions
44
activerecord/lib/active_record/mass_assignment_security/permission_set.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
require 'active_record/mass_assignment_security/sanitizer' | ||
|
||
module ActiveRecord | ||
module MassAssignmentSecurity | ||
class PermissionSet < Set | ||
|
||
attr_accessor :logger | ||
|
||
def merge(values) | ||
super(values.map(&:to_s)) | ||
end | ||
|
||
def include?(key) | ||
super(remove_multiparameter_id(key)) | ||
end | ||
|
||
protected | ||
|
||
def remove_multiparameter_id(key) | ||
key.gsub(/\(.+/, '') | ||
end | ||
|
||
end | ||
|
||
class WhiteList < PermissionSet | ||
include Sanitizer | ||
|
||
def deny?(key) | ||
!include?(key) | ||
end | ||
|
||
end | ||
|
||
class BlackList < PermissionSet | ||
include Sanitizer | ||
|
||
def deny?(key) | ||
include?(key) | ||
end | ||
|
||
end | ||
|
||
end | ||
end |
Oops, something went wrong.
606088d
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This commit caused some working code in my friendly_id plugin to fail; previously
accessible_attributes
returned nil ifattr_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.606088d
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's indeed intentional.