1
0
Fork 0
mirror of https://github.com/heartcombo/devise.git synced 2022-11-09 12:18:31 -05:00

Simplify parameter sanitization proposal

This commit is contained in:
José Valim 2013-08-11 22:18:29 +02:00
parent 5e7caffc9e
commit 4e318b5167
7 changed files with 75 additions and 108 deletions

View file

@ -3,10 +3,15 @@
* backwards incompatible changes * backwards incompatible changes
* Do not store confirmation, unlock and reset password tokens directly in the database. This means tokens previously stored in the database are no longer valid. You can reenable this temporarily by setting `config.allow_insecure_tokens_lookup = true` in your configuration file. It is recommended to keep this configuration set to true just temporarily in your production servers only to aid migration * Do not store confirmation, unlock and reset password tokens directly in the database. This means tokens previously stored in the database are no longer valid. You can reenable this temporarily by setting `config.allow_insecure_tokens_lookup = true` in your configuration file. It is recommended to keep this configuration set to true just temporarily in your production servers only to aid migration
* The Devise mailer and its views were changed to explicitly receive a token as argument. You will need to update your mailers and re-copy the views to your application with `rails g devise:views` * The Devise mailer and its views were changed to explicitly receive a token as argument. You will need to update your mailers and re-copy the views to your application with `rails g devise:views`
* Sanitization of parameters should be done by calling `devise_parameter_sanitizier.sanitize(:action)` instead of `devise_parameter_sanitizier.for(:action)`
* deprecations * deprecations
* Token authentication is deprecated * Token authentication is deprecated
* enhancements
* Better security defaults
* Allow easier customization of parameter sanitizer
* bug fix * bug fix
* Do not sign in after confirmation * Do not sign in after confirmation
* Do not store confirmation, unlock and reset password tokens directly in the database * Do not store confirmation, unlock and reset password tokens directly in the database

View file

@ -196,23 +196,15 @@ class ApplicationController < ActionController::Base
protected protected
def configure_permitted_parameters def configure_permitted_parameters
# permit parameters for all actions devise_parameter_sanitizer.for(:sign_up) << :username
devise_permitted_parameters.add(:username, :age)
# permit a parameter for a single action
devise_permitted_parameters.for(:sign_up) << :hometown
end end
end end
``` ```
To remove or overwrite the defaults that Devise provides: To completely change Devise defaults or invoke custom behaviour, you can also pass a block:
```ruby ```ruby
def configure_permitted_parameters def configure_permitted_parameters
# remove a permitted parameter
devise_permitted_parameters.remove(:email)
# overwrite the Devise defaults
devise_parameter_sanitizer.for(:sign_in) { |u| u.permit(:username, :email) } devise_parameter_sanitizer.for(:sign_in) { |u| u.permit(:username, :email) }
end end
``` ```
@ -267,7 +259,7 @@ rails generate devise:views users
If the customization at the views level is not enough, you can customize each controller by following these steps: If the customization at the views level is not enough, you can customize each controller by following these steps:
1. Create your custom controller, for example a `Admins::SessionsController`: 1. Create your custom controller, for example a `Admins::SessionsController`:
```ruby ```ruby
class Admins::SessionsController < Devise::SessionsController class Admins::SessionsController < Devise::SessionsController

View file

@ -117,10 +117,10 @@ class Devise::RegistrationsController < DeviseController
end end
def sign_up_params def sign_up_params
devise_parameter_sanitizer.for(:sign_up) devise_parameter_sanitizer.sanitize(:sign_up)
end end
def account_update_params def account_update_params
devise_parameter_sanitizer.for(:account_update) devise_parameter_sanitizer.sanitize(:account_update)
end end
end end

View file

@ -35,7 +35,7 @@ class Devise::SessionsController < DeviseController
protected protected
def sign_in_params def sign_in_params
devise_parameter_sanitizer.for(:sign_in) devise_parameter_sanitizer.sanitize(:sign_in)
end end
def serialize_options(resource) def serialize_options(resource)

View file

@ -91,10 +91,6 @@ module Devise
end end
end end
def devise_permitted_parameters
devise_parameter_sanitizer.permitted_parameters
end
# Tell warden that params authentication is allowed for that specific page. # Tell warden that params authentication is allowed for that specific page.
def allow_params_authentication! def allow_params_authentication!
request.env["devise.allow_params_authentication"] = true request.env["devise.allow_params_authentication"] = true

View file

@ -13,14 +13,23 @@ module Devise
if block_given? if block_given?
@blocks[kind] = block @blocks[kind] = block
else else
block = @blocks[kind] default_for(kind)
block ? block.call(default_params) : fallback_for(kind) end
end
def sanitize(kind)
if block = @blocks[kind]
block.call(default_params)
elsif respond_to?(kind, true)
send(kind)
else
raise NotImplementedError, "Devise doesn't know how to sanitize parameters for #{kind}"
end end
end end
private private
def fallback_for(kind) def default_for(kind)
default_params default_params
end end
@ -30,61 +39,45 @@ module Devise
end end
class ParameterSanitizer < BaseSanitizer class ParameterSanitizer < BaseSanitizer
def initialize(*)
class PermittedParameters super
@permitted = Hash.new { |h,k| h[k] = attributes_for(k) }
def initialize(resource_class)
@resource_class = resource_class
@for = { :sign_in => sign_in, :sign_up => sign_up, :account_update => account_update }
end
def sign_in
auth_keys + [:password, :remember_me]
end
def sign_up
auth_keys + [:password, :password_confirmation]
end
def account_update
auth_keys + [:password, :password_confirmation, :current_password]
end
def auth_keys
@resource_class.authentication_keys.respond_to?(:keys) ? @resource_class.authentication_keys.keys : @resource_class.authentication_keys
end
def for(kind)
@for[kind]
end
def add(*params)
@for.each { |action, permitted| permitted.push *params }
end
def remove(*params)
@for.each do |action, permitted|
permitted.delete_if { |param| params.include? param }
end
end
end end
def permitted_parameters def sign_in
@permitted_parameters ||= PermittedParameters.new(@resource_class) default_params.permit self.for(:sign_in)
end
def sign_up
default_params.permit self.for(:sign_up)
end
def account_update
default_params.permit self.for(:account_update)
end end
private private
def fallback_for(kind) # Change for(kind) to return the values in the @permitted
if respond_to?(kind, true) # hash, allowing the developer to customize at runtime.
send(kind) def default_for(kind)
elsif (permitted = permitted_parameters.for(kind)) @permitted[kind] || raise("No sanitizer provided for #{kind}")
default_params.permit permitted end
else
raise NotImplementedError, "Devise Parameter Sanitizer doesn't know how to sanitize parameters for #{kind}" def attributes_for(kind)
case kind
when :sign_in
auth_keys + [:password, :remember_me]
when :sign_up
auth_keys + [:password, :password_confirmation]
when :account_update
auth_keys + [:password, :password_confirmation, :current_password]
end end
end end
def auth_keys
@auth_keys ||= @resource_class.authentication_keys.respond_to?(:keys) ?
@resource_class.authentication_keys.keys : @resource_class.authentication_keys
end
end end
end end

View file

@ -2,13 +2,21 @@ require 'test_helper'
require 'devise/parameter_sanitizer' require 'devise/parameter_sanitizer'
class BaseSanitizerTest < ActiveSupport::TestCase class BaseSanitizerTest < ActiveSupport::TestCase
def sanitizer def sanitizer(params)
Devise::BaseSanitizer.new(User, :user, { user: { "email" => "jose" } }) params = ActionController::Parameters.new(params)
Devise::BaseSanitizer.new(User, :user, params)
end end
test 'returns chosen params' do test 'returns chosen params' do
sanitizer = sanitizer(user: { "email" => "jose" })
assert_equal({ "email" => "jose" }, sanitizer.for(:sign_in)) assert_equal({ "email" => "jose" }, sanitizer.for(:sign_in))
end end
test 'allow custom blocks' do
sanitizer = sanitizer(user: { "email" => "jose", "password" => "invalid" })
sanitizer.for(:sign_in) { |user| user.permit(:email) }
assert_equal({ "email" => "jose" }, sanitizer.sanitize(:sign_in))
end
end end
if defined?(ActionController::StrongParameters) if defined?(ActionController::StrongParameters)
@ -22,76 +30,49 @@ if defined?(ActionController::StrongParameters)
test 'filters some parameters on sign in by default' do test 'filters some parameters on sign in by default' do
sanitizer = sanitizer(user: { "email" => "jose", "password" => "invalid", "remember_me" => "1" }) sanitizer = sanitizer(user: { "email" => "jose", "password" => "invalid", "remember_me" => "1" })
assert_equal({ "email" => "jose", "password" => "invalid", "remember_me" => "1" }, sanitizer.for(:sign_in)) assert_equal({ "email" => "jose", "password" => "invalid", "remember_me" => "1" }, sanitizer.sanitize(:sign_in))
end end
test 'handles auth keys as a hash' do test 'handles auth keys as a hash' do
swap Devise, :authentication_keys => {:email => true} do swap Devise, :authentication_keys => {:email => true} do
sanitizer = sanitizer(user: { "email" => "jose", "password" => "invalid" }) sanitizer = sanitizer(user: { "email" => "jose", "password" => "invalid" })
assert_equal({ "email" => "jose", "password" => "invalid" }, sanitizer.for(:sign_in)) assert_equal({ "email" => "jose", "password" => "invalid" }, sanitizer.sanitize(:sign_in))
end end
end end
test 'filters some parameters on sign up by default' do test 'filters some parameters on sign up by default' do
sanitizer = sanitizer(user: { "email" => "jose", "role" => "invalid" }) sanitizer = sanitizer(user: { "email" => "jose", "role" => "invalid" })
assert_equal({ "email" => "jose" }, sanitizer.for(:sign_up)) assert_equal({ "email" => "jose" }, sanitizer.sanitize(:sign_up))
end end
test 'filters some parameters on account update by default' do test 'filters some parameters on account update by default' do
sanitizer = sanitizer(user: { "email" => "jose", "role" => "invalid" }) sanitizer = sanitizer(user: { "email" => "jose", "role" => "invalid" })
assert_equal({ "email" => "jose" }, sanitizer.for(:account_update)) assert_equal({ "email" => "jose" }, sanitizer.sanitize(:account_update))
end end
test 'allows custom hooks' do test 'allows custom hooks' do
sanitizer = sanitizer(user: { "email" => "jose", "password" => "invalid" }) sanitizer = sanitizer(user: { "email" => "jose", "password" => "invalid" })
sanitizer.for(:sign_in) { |user| user.permit(:email, :password) } sanitizer.for(:sign_in) { |user| user.permit(:email, :password) }
assert_equal({ "email" => "jose", "password" => "invalid" }, sanitizer.for(:sign_in)) assert_equal({ "email" => "jose", "password" => "invalid" }, sanitizer.sanitize(:sign_in))
end
test 'adding permitted parameters for a single action' do
sanitizer = sanitizer(user: { "email" => "jose", "username" => "jose1" })
sanitizer.permitted_parameters.for(:sign_up).push(:username)
assert_equal({ "email" => "jose", "username" => "jose1" }, sanitizer.for(:sign_up))
assert_equal({ "email" => "jose" }, sanitizer.for(:sign_in))
end
test 'adding permitted parameters for all actions' do
sanitizer = sanitizer(user: { "email" => "jose", "username" => "jose1" })
sanitizer.permitted_parameters.add(:username)
assert_equal({ "email" => "jose", "username" => "jose1" }, sanitizer.for(:sign_in))
assert_equal({ "email" => "jose", "username" => "jose1" }, sanitizer.for(:sign_up))
assert_equal({ "email" => "jose", "username" => "jose1" }, sanitizer.for(:account_update))
end
test 'removing default parameters' do
sanitizer = sanitizer(user: { "email" => "jose", "password" => "invalid" })
sanitizer.permitted_parameters.remove(:email)
assert_equal({ "password" => "invalid" }, sanitizer.for(:sign_in))
assert_equal({ "password" => "invalid" }, sanitizer.for(:sign_up))
assert_equal({ "password" => "invalid" }, sanitizer.for(:account_update))
end end
test 'adding multiple permitted parameters' do test 'adding multiple permitted parameters' do
sanitizer = sanitizer(user: { "email" => "jose", "username" => "jose1", "role" => "valid" }) sanitizer = sanitizer(user: { "email" => "jose", "username" => "jose1", "role" => "valid" })
sanitizer.for(:sign_in).concat([:username, :role])
sanitizer.permitted_parameters.add(:username, :role) assert_equal({ "email" => "jose", "username" => "jose1", "role" => "valid" }, sanitizer.sanitize(:sign_in))
assert_equal({ "email" => "jose", "username" => "jose1", "role" => "valid" }, sanitizer.for(:sign_in))
end end
test 'removing multiple default parameters' do test 'removing multiple default parameters' do
sanitizer = sanitizer(user: { "email" => "jose", "password" => "invalid", "remember_me" => "1" }) sanitizer = sanitizer(user: { "email" => "jose", "password" => "invalid", "remember_me" => "1" })
sanitizer.permitted_parameters.remove(:email, :password) sanitizer.for(:sign_in).delete(:email)
sanitizer.for(:sign_in).delete(:password)
assert_equal({ "remember_me" => "1" }, sanitizer.for(:sign_in)) assert_equal({ "remember_me" => "1" }, sanitizer.sanitize(:sign_in))
end end
test 'raises on unknown hooks' do test 'raises on unknown hooks' do
sanitizer = sanitizer(user: { "email" => "jose", "password" => "invalid" }) sanitizer = sanitizer(user: { "email" => "jose", "password" => "invalid" })
assert_raise NotImplementedError do assert_raise NotImplementedError do
sanitizer.for(:unknown) sanitizer.sanitize(:unknown)
end end
end end
end end