Permalink
Browse files

initial version to keep your controllers secure by default

  • Loading branch information...
0 parents commit 97e8705afcf484c526e17ebe0f4bc65c06a0e225 @dcunning dcunning committed Mar 15, 2012
Showing with 849 additions and 0 deletions.
  1. +19 −0 .gitignore
  2. +4 −0 Gemfile
  3. +22 −0 LICENSE
  4. +93 −0 README.md
  5. +7 −0 Rakefile
  6. +8 −0 lib/param_accessible.rb
  7. +41 −0 lib/param_accessible/controller_ext.rb
  8. +19 −0 lib/param_accessible/error.rb
  9. +23 −0 lib/param_accessible/not_acceptable_helper.rb
  10. +93 −0 lib/param_accessible/rule.rb
  11. +58 −0 lib/param_accessible/rules.rb
  12. +3 −0 lib/param_accessible/version.rb
  13. +23 −0 param_accessible.gemspec
  14. +34 −0 spec/app_root/app/controllers/application_controller.rb
  15. +6 −0 spec/app_root/app/controllers/except_controller.rb
  16. +12 −0 spec/app_root/app/controllers/if_false_controller.rb
  17. +12 −0 spec/app_root/app/controllers/if_true_controller.rb
  18. +6 −0 spec/app_root/app/controllers/merge_controller.rb
  19. +6 −0 spec/app_root/app/controllers/not_acceptable_controller.rb
  20. +6 −0 spec/app_root/app/controllers/only_controller.rb
  21. +4 −0 spec/app_root/app/controllers/simple_controller.rb
  22. +12 −0 spec/app_root/app/controllers/unless_false_controller.rb
  23. +12 −0 spec/app_root/app/controllers/unless_true_controller.rb
  24. +12 −0 spec/app_root/config/application.rb
  25. +5 −0 spec/app_root/config/environment.rb
  26. +3 −0 spec/app_root/config/routes.rb
  27. +34 −0 spec/lib/except_spec.rb
  28. +24 −0 spec/lib/if_false_spec.rb
  29. +34 −0 spec/lib/if_true_spec.rb
  30. +20 −0 spec/lib/merge_spec.rb
  31. +30 −0 spec/lib/not_acceptable_helper_spec.rb
  32. +34 −0 spec/lib/only_spec.rb
  33. +57 −0 spec/lib/simple_spec.rb
  34. +34 −0 spec/lib/unless_false_spec.rb
  35. +24 −0 spec/lib/unless_true_spec.rb
  36. +15 −0 spec/spec_helper.rb
@@ -0,0 +1,19 @@
+*.gem
+*.rbc
+.bundle
+.config
+.yardoc
+.rspec
+Gemfile.lock
+InstalledFiles
+_yardoc
+coverage
+doc/
+lib/bundler/man
+pkg
+rdoc
+spec/reports
+spec/app_root/log
+test/tmp
+test/version_tmp
+tmp
@@ -0,0 +1,4 @@
+source 'https://rubygems.org'
+
+# Specify your gem's dependencies in param_accessible.gemspec
+gemspec
22 LICENSE
@@ -0,0 +1,22 @@
+Copyright (c) 2012 Dan Cunning
+
+MIT License
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,93 @@
+# ParamAccessible
+
+[![Build Status](https://secure.travis-ci.org/topdan/param_accessible.png)](https://secure.travis-ci.org/topdan/param_accessible.png)
+
+Provides a method to help protect your Ruby on Rails controllers from malicious or accidentally destructive user parameters. It is independent, but heavily influenced by param_protected.
+
+Make all your controllers secure by default as well as provide readable messages to users when a security breach was prevented.
+
+For more information on the design considerations please visit: https://www.topdan.com/ruby-on-rails/params-accessible.html
+
+## Installation
+
+Add this line to your application's Gemfile:
+
+ gem 'param_accessible'
+
+And then execute:
+
+ $ bundle
+
+Or install it yourself as:
+
+ $ gem install param_accessible
+
+## Usage
+
+ class ApplicationController < ActionController::Base
+
+ # make all your controllers secure by default
+ before_filter :ensure_params_are_accessible, :only => [:create, :update]
+
+ # expose the common rails parameters
+ param_accessible :controller, :action, :format, :id
+
+ # this error is thrown when the user tries to access an inaccessible param
+ rescue_from ParamAccessible::Error, :with => :handle_param_not_accessible
+
+ protected
+
+ def handle_param_not_accessible e
+ flash[:error] = "You gave me some invalid parameters: #{e.inaccessible_params.join(', )}"
+ redirect_to :back
+ end
+
+ end
+
+ class UserController < ApplicationController
+
+ # these attributes are available for everyone
+ param_accessible :user => {:name, :email, :password, :password_confirmation}
+
+ # these attributes are only available if the controller instance method is_admin? is true
+ param_accessible :user => {:is_admin, :is_locked_out}, :if => :is_admin?
+
+ def update
+ @user = User.find(params[:id])
+
+ # this is now safe!
+ if @user.update_attributes(params[:user])
+ ...
+ else
+ ...
+ end
+ end
+ end
+
+ class DemoController < ApplicationController
+
+ # rescue_from ParamAccessible::Error and respond with a 406 Not Acceptable status
+ # with an HTML, JSON, XML, or JS explanation of which parameters were invalid
+ include ParamAccessible::NotAcceptableHelper
+
+ param_accessible :foo, :if => :is_admin
+ param_accessible :bar, :unless => :logged_in?
+ param_accessible :baz, :only => :show
+ param_accessible :nut, :except => :index
+
+ end
+
+ class InsecureController < ApplicationController
+
+ # skip the filter ApplicationController set up to avoid the accessible parameter checks
+ skip_before_filter :ensure_params_are_accessible
+
+ end
+
+## Contributing
+
+1. Fork it
+2. Create your feature branch (`git checkout -b my-new-feature`)
+3. Commit your changes (`git commit -am 'Added some feature'`)
+4. Push to the branch (`git push origin my-new-feature`)
+5. Create new Pull Request
@@ -0,0 +1,7 @@
+#!/usr/bin/env rake
+require "bundler/gem_tasks"
+require 'rspec/core/rake_task'
+RSpec::Core::RakeTask.new('spec')
+
+task :test => :spec
+task :default => :test
@@ -0,0 +1,8 @@
+require "param_accessible/version"
+require "param_accessible/error"
+require "param_accessible/rule"
+require "param_accessible/rules"
+require "param_accessible/not_acceptable_helper"
+require "param_accessible/controller_ext"
+
+ActionController::Base.send(:include, ParamAccessible::ControllerExt)
@@ -0,0 +1,41 @@
+module ParamAccessible
+
+ module ControllerExt
+ extend ActiveSupport::Concern
+
+ protected
+
+ def ensure_params_are_accessible
+ inaccessible_params = param_accessible_rules.detect_inaccessible_params self
+
+ unless inaccessible_params.nil? || inaccessible_params.blank?
+ raise ParamAccessible::Error.new(inaccessible_params)
+ end
+ end
+
+ def param_accessible_rules
+ self.class.param_accessible_rules
+ end
+
+ module ClassMethods
+
+ def param_accessible *args
+ param_accessible_rules.push *args
+ end
+
+ def param_accessible_rules
+ return @param_accessible_rules if defined? @param_accessible_rules
+
+ # inheritance
+ if superclass.respond_to?(:param_accessible_rules)
+ @param_accessible_rules = Rules.new superclass.param_accessible_rules
+ else
+ @param_accessible_rules = Rules.new
+ end
+ end
+
+ end
+
+ end
+
+end
@@ -0,0 +1,19 @@
+module ParamAccessible
+
+ class Error < Exception
+
+ attr_reader :inaccessible_params
+
+ def initialize inaccessible_params
+ if inaccessible_params.length == 1
+ super "#{inaccessible_params.join(', ')} is an invalid parameter"
+ else
+ super "#{inaccessible_params.join(', ')} are invalid parameters"
+ end
+
+ @inaccessible_params = inaccessible_params
+ end
+
+ end
+
+end
@@ -0,0 +1,23 @@
+module ParamAccessible
+
+ module NotAcceptableHelper
+ extend ActiveSupport::Concern
+
+ included do
+ rescue_from ParamAccessible::Error, :with => :handle_param_not_accessible
+ end
+
+ protected
+
+ def handle_param_not_accessible error
+ respond_to do |format|
+ format.html { render :status => 406, :text => error.message }
+ format.json { render :status => 406, :json => {:error => {:message => "You supplied invalid parameters: #{error.inaccessible_params.join(', ')}"}} }
+ format.xml { render :status => 406, :xml => {:message => "You supplied invalid parameters: #{error.inaccessible_params.join(', ')}"}.to_xml('error') }
+ format.js { render :status => 406, :text => %(// invalid parameters: #{error.inaccessible_params.join(', ')}\n) }
+ end
+ end
+
+ end
+
+end
@@ -0,0 +1,93 @@
+module ParamAccessible
+
+ class Rule
+
+ attr_reader :attributes
+ attr_reader :if_option, :unless_option
+ attr_reader :only_options, :except_options
+
+ def initialize *args
+ if args.length > 1 && args.last.is_a?(Hash)
+ options = args.last
+ attributes = args[0..-2]
+
+ options.assert_valid_keys :if, :unless, :only, :except
+ # options = normalize_options options
+ else
+ options = {}
+ attributes = args
+ end
+
+ @if_option = options[:if]
+ @unless_option = options[:unless]
+
+ @only_options = clean_action_option options[:only]
+ @except_options = clean_action_option options[:except]
+
+ @attributes = normalize_params attributes
+ end
+
+ def clean_action_option value
+ return if value == nil
+ value = [value] unless value.is_a?(Array)
+ value.collect {|v| v.to_s }
+ end
+
+ def accessible_params_for controller, dest
+ return if @if_option != nil && !controller.send(@if_option)
+ return if @unless_option != nil && controller.send(@unless_option)
+
+ return if @only_options != nil && !@only_options.include?(controller.action_name)
+ return if @except_options != nil && @except_options.include?(controller.action_name)
+
+ accessible_hash_for controller, @attributes, dest
+ end
+
+ protected
+
+ def accessible_hash_for controller, attributes, dest
+ attributes.each do |key, value|
+ if value.is_a?(Hash)
+ attrs = dest[key]
+ if attrs.nil?
+ attrs = {}
+ dest[key] = attrs
+ end
+
+ accessible_hash_for controller, value, attrs
+ else
+ dest[key] = value
+ end
+ end
+ end
+
+ # When specifying params to protect, we allow a combination of arrays and hashes much like how
+ # ActiveRecord::Base#find's :include options works. This method normalizes that into just nested hashes,
+ # stringifying the keys and setting all values to nil. This format is easier/faster to work with when
+ # filtering the controller params.
+ # Example...
+ # [:a, {:b => [:c, :d]}]
+ # to
+ # {"a"=>nil, "b"=>{"c"=>nil, "d"=>nil}}
+ def normalize_params(params, params_out = {})
+ if params.instance_of?(Array)
+ params.each{ |param| normalize_params(param, params_out) }
+ elsif params.instance_of?(Hash)
+ params.each do |k, v|
+ k = normalize_key(k)
+ params_out[k] = {}
+ normalize_params(v, params_out[k])
+ end
+ else
+ params_out[normalize_key(params)] = nil
+ end
+ params_out
+ end
+
+ def normalize_key(k)
+ k.to_s
+ end
+
+ end
+
+end
@@ -0,0 +1,58 @@
+module ParamAccessible
+
+ class Rules < Array
+
+ def initialize parent = nil
+ content = (parent.to_a if parent) || []
+ super content
+ end
+
+ def detect_inaccessible_params controller
+ accessible_params = {}
+
+ each do |rule|
+ rule.accessible_params_for controller, accessible_params
+ end
+
+ detect_inaccessible_hash controller.params, accessible_params, []
+ end
+
+ def push *args
+ super Rule.new(*args)
+ end
+
+ protected
+
+ def detect_inaccessible_hash hash, accessible, errors, prefix = nil
+ hash.each do |key, value|
+ if !accessible.has_key?(key)
+ errors.push prefix_for(prefix, key)
+
+ elsif value.is_a?(Hash)
+ nested = accessible[key] || {}
+ detect_inaccessible_hash value, nested, errors, prefix_for(prefix, key)
+
+ elsif value.is_a?(Array)
+ nested = accessible[key] || {}
+ value.each do |v|
+ if v.is_a?(Hash)
+ detect_inaccessible_hash v, nested, errors, prefix_for(prefix, key)
+ end
+ end
+ end
+ end
+
+ errors
+ end
+
+ def prefix_for prefix, key
+ if prefix
+ "#{prefix}[#{key}]"
+ else
+ key
+ end
+ end
+
+ end
+
+end
Oops, something went wrong.

0 comments on commit 97e8705

Please sign in to comment.