Permalink
Browse files

Merge pull request #1100 from skade/authenticity_token

Add csrf protection through authenticity_token
  • Loading branch information...
skade committed Mar 13, 2013
2 parents 2dbcc2d + 1163c34 commit d7f719db9f6f863811c22978ba1329645e828ccb
@@ -180,7 +180,7 @@ def default_configuration!
set :public_folder, Proc.new { Padrino.root('public', uri_root) }
set :views, Proc.new { File.join(root, 'views') }
set :images_path, Proc.new { File.join(public_folder, 'images') }
set :protection, false
set :protection, true
# Haml specific
set :haml, { :ugly => (Padrino.env == :production) } if defined?(Haml)
# Padrino specific
@@ -190,6 +190,9 @@ def default_configuration!
set :authentication, false
# Padrino locale
set :locale_path, Proc.new { Dir[File.join(settings.root, '/locale/**/*.{rb,yml}')] }
# Authenticity token
set :protect_from_csrf, false
set :allow_disabled_csrf, false
# Load the Global Configurations
class_eval(&Padrino.apps_configuration) if Padrino.apps_configuration
end
@@ -254,8 +257,35 @@ def setup_default_middleware(builder)
builder.use Rack::Head
register Padrino::Flash
setup_protection builder
setup_csrf_protection builder
setup_application!
end
# sets up csrf protection for the app
def setup_csrf_protection(builder)
if protect_from_csrf? && !sessions?
raise(<<-ERROR)
`protect_from_csrf` is activated, but `sessions` are not. To enable csrf
protection, use:
enable :sessions
or deactivate protect_from_csrf:
disable :protect_from_csrf
ERROR
end
if protect_from_csrf?
if allow_disabled_csrf?
builder.use Rack::Protection::AuthenticityToken,
:reaction => :report,
:report_key => 'protection.csrf.failed'
else
builder.use Rack::Protection::AuthenticityToken
end
end
end
end # self
end # Application
end # Padrino
@@ -559,6 +559,14 @@ def route(verb, path, *args, &block)
# Do padrino parsing. We dup options so we can build HEAD request correctly
route_options = options.dup
route_options[:provides] = @_provides if @_provides
# CSRF protection is always active except when explicitly switched off
if allow_disabled_csrf
unless route_options[:csrf_protection] == false
route_options[:csrf_protection] = true
end
end
path, *route_options[:with] = path if path.is_a?(Array)
action = path
path, name, route_parents, options, route_options = *parse_route(path, route_options, verb)
@@ -799,6 +807,21 @@ def provides(*types)
matched_format
end
end
##
# Implements CSRF checking when `allow_disabled_csrf` is set to true.
#
# This condition is always on, except when it is explicitly switched
# off.
#
# @example
# post("/", :csrf_protection => false)
#
def csrf_protection(on = true)
if on
condition { halt 403 if request.env['protection.csrf.failed'] }
end
end
end
##
@@ -39,4 +39,5 @@ Gem::Specification.new do |s|
s.add_dependency("http_router", "~> 0.10.2")
s.add_dependency("thor", "~> 0.16.0")
s.add_dependency("activesupport", ">= 3.1.0")
s.add_dependency("rack-protection", ">= 1.5.0")
end
@@ -0,0 +1,80 @@
require File.expand_path(File.dirname(__FILE__) + '/helper')
describe "Application" do
before { Padrino.clear! }
after { remove_views }
context 'CSRF protection' do
context "with CSRF protection on" do
before do
mock_app do
enable :sessions
enable :protect_from_csrf
post('/'){ 'HI' }
end
end
should "not allow requests without tokens" do
post "/"
assert_equal 403, status
end
should "allow requests with correct tokens" do
post "/", {"authenticity_token" => "a"}, 'rack.session' => {:csrf => "a"}
assert_equal 200, status
end
should "not allow requests with incorrect tokens" do
post "/", {"authenticity_token" => "a"}, 'rack.session' => {:csrf => "b"}
assert_equal 403, status
end
end
context "without CSRF protection on" do
before do
mock_app do
enable :sessions
disable :protect_from_csrf
post('/'){ 'HI' }
end
end
should "allows requests without tokens" do
post "/"
assert_equal 200, status
end
should "allow requests with correct tokens" do
post "/", {"authenticity_token" => "a"}, 'rack.session' => {:csrf => "a"}
assert_equal 200, status
end
should "allow requests with incorrect tokens" do
post "/", {"authenticity_token" => "a"}, 'rack.session' => {:csrf => "b"}
assert_equal 200, status
end
end
context "with optional CSRF protection" do
before do
mock_app do
enable :sessions
enable :protect_from_csrf
set :allow_disabled_csrf, true
post('/on') { 'HI' }
post('/off', :csrf_protection => false) { 'HI' }
end
end
should "allow access to routes with csrf_protection off" do
post "/off"
assert_equal 200, status
end
should "not allow access to routes with csrf_protection on" do
post "/on"
assert_equal 403, status
end
end
end
end
@@ -28,6 +28,8 @@
Padrino.configure_apps do
# enable :sessions
set :session_secret, '<%= SecureRandom.hex(32) %>'
set :protection, true
set :protect_from_csrf, true
end
# Mounts the core application for this project
@@ -169,6 +169,10 @@ def fields_for(child_association, instance_or_collection=nil, &block)
end.join("\n").html_safe
end
def csrf_token_field
@template.csrf_token_field
end
protected
# Returns the known field types for a formbuilder
def self.field_types
@@ -1,3 +1,5 @@
require 'securerandom'
module Padrino
module Helpers
##
@@ -28,7 +30,9 @@ module FormHelpers
#
# @api public
def form_for(object, url, settings={}, &block)
form_html = capture_html(builder_instance(object, settings), &block)
instance = builder_instance(object, settings)
form_html = instance.csrf_token_field
form_html << capture_html(instance, &block)
form_tag(url, settings) { form_html }
end
@@ -691,6 +695,25 @@ def image_submit_tag(source, options={})
input_tag(:image, options)
end
# Constructs a hidden field containing a CSRF token.
#
# @param [String] token
# The token to use. Will be read from the session by default.
#
# @return [String] The hidden field with CSRF token as value.
#
# @example
# csrf_token_field
#
# @api public
def csrf_token_field(token = nil)
if defined? session
token ||= (session[:csrf] ||= SecureRandom.hex(32))
end
hidden_field_tag :authenticity_token, :value => token
end
protected
##
@@ -103,20 +103,23 @@ def standard_builder(object=@user)
assert_have_selector :form, :action => '/demo', :id => 'demo'
assert_have_selector :form, :action => '/another_demo', :id => 'demo2', :method => 'get'
assert_have_selector :form, :action => '/third_demo', :id => 'demo3', :method => 'get'
assert_have_selector :input, :name => 'authenticity_token'
end
should "display correct form in erb" do
visit '/erb/form_for'
assert_have_selector :form, :action => '/demo', :id => 'demo'
assert_have_selector :form, :action => '/another_demo', :id => 'demo2', :method => 'get'
assert_have_selector :form, :action => '/third_demo', :id => 'demo3', :method => 'get'
assert_have_selector :input, :name => 'authenticity_token'
end
should "display correct form in slim" do
visit '/slim/form_for'
assert_have_selector :form, :action => '/demo', :id => 'demo'
assert_have_selector :form, :action => '/another_demo', :id => 'demo2', :method => 'get'
assert_have_selector :form, :action => '/third_demo', :id => 'demo3', :method => 'get'
assert_have_selector :input, :name => 'authenticity_token'
end
should "have a class of 'invalid' for fields with errors" do

0 comments on commit d7f719d

Please sign in to comment.