public forms are now protected from csrf attacks (see issue #382

This commit is contained in:
did 2012-05-23 15:03:21 -07:00
commit d90123e321
14 changed files with 160 additions and 29 deletions

View File

@ -45,7 +45,7 @@ PATH
mimetype-fu (~> 0.1.2) mimetype-fu (~> 0.1.2)
mongo (~> 1.5.2) mongo (~> 1.5.2)
mongoid (~> 2.4.9) mongoid (~> 2.4.9)
multi_json (= 1.3.4) multi_json (~> 1.3.4)
rack-cache (~> 1.1) rack-cache (~> 1.1)
rails (~> 3.2.3) rails (~> 3.2.3)
rails-backbone (~> 0.6.1) rails-backbone (~> 0.6.1)
@ -109,7 +109,7 @@ GEM
carrierwave-mongoid (0.1.3) carrierwave-mongoid (0.1.3)
carrierwave (>= 0.5.6) carrierwave (>= 0.5.6)
mongoid (~> 2.1) mongoid (~> 2.1)
cells (3.8.3) cells (3.8.5)
actionpack (~> 3.0) actionpack (~> 3.0)
railties (~> 3.0) railties (~> 3.0)
childprocess (0.3.2) childprocess (0.3.2)
@ -123,13 +123,12 @@ GEM
coffee-script (2.2.0) coffee-script (2.2.0)
coffee-script-source coffee-script-source
execjs execjs
coffee-script-source (1.3.1) coffee-script-source (1.3.3)
cucumber (1.1.9) cucumber (1.2.0)
builder (>= 2.1.2) builder (>= 2.1.2)
diff-lcs (>= 1.1.2) diff-lcs (>= 1.1.3)
gherkin (~> 2.9.0) gherkin (~> 2.10.0)
json (>= 1.4.6) json (>= 1.4.6)
term-ansicolor (>= 1.0.6)
cucumber-rails (1.3.0) cucumber-rails (1.3.0)
capybara (>= 1.1.2) capybara (>= 1.1.2)
cucumber (>= 1.1.8) cucumber (>= 1.1.8)
@ -149,7 +148,7 @@ GEM
ejs (1.0.0) ejs (1.0.0)
erubis (2.7.0) erubis (2.7.0)
excon (0.13.4) excon (0.13.4)
execjs (1.3.1) execjs (1.4.0)
multi_json (~> 1.0) multi_json (~> 1.0)
factory_girl (2.5.2) factory_girl (2.5.2)
activesupport (>= 2.3.9) activesupport (>= 2.3.9)
@ -169,14 +168,14 @@ GEM
net-ssh (>= 2.1.3) net-ssh (>= 2.1.3)
nokogiri (~> 1.5.0) nokogiri (~> 1.5.0)
ruby-hmac ruby-hmac
formatador (0.2.1) formatador (0.2.3)
formtastic (2.0.2) formtastic (2.0.2)
rails (~> 3.0) rails (~> 3.0)
fssm (0.2.9) fssm (0.2.9)
gherkin (2.9.3) gherkin (2.10.0)
json (>= 1.4.6) json (>= 1.4.6)
haml (3.1.4) haml (3.1.6)
highline (1.6.11) highline (1.6.12)
hike (1.2.1) hike (1.2.1)
httparty (0.8.3) httparty (0.8.3)
multi_json (~> 1.0) multi_json (~> 1.0)
@ -186,7 +185,8 @@ GEM
jquery-rails (1.0.19) jquery-rails (1.0.19)
railties (~> 3.0) railties (~> 3.0)
thor (~> 0.14) thor (~> 0.14)
json (1.7.0) jruby-pageant (1.0.2)
json (1.7.3)
json_spec (1.0.3) json_spec (1.0.3)
multi_json (~> 1.0) multi_json (~> 1.0)
rspec (~> 2.0) rspec (~> 2.0)
@ -215,15 +215,16 @@ GEM
mocha (0.9.12) mocha (0.9.12)
mongo (1.5.2) mongo (1.5.2)
bson (= 1.5.2) bson (= 1.5.2)
mongoid (2.4.9) mongoid (2.4.10)
activemodel (~> 3.1) activemodel (~> 3.1)
mongo (~> 1.3) mongo (~> 1.3)
tzinfo (~> 0.3.22) tzinfo (~> 0.3.22)
multi_json (1.3.4) multi_json (1.3.5)
multi_xml (0.4.4) multi_xml (0.5.1)
net-scp (1.0.4) net-scp (1.0.4)
net-ssh (>= 1.99.1) net-ssh (>= 1.99.1)
net-ssh (2.3.0) net-ssh (2.4.0)
jruby-pageant (>= 1.0.2)
nokogiri (1.5.2) nokogiri (1.5.2)
orm_adapter (0.0.7) orm_adapter (0.0.7)
pickle (0.4.10) pickle (0.4.10)
@ -256,7 +257,7 @@ GEM
rake (>= 0.8.7) rake (>= 0.8.7)
rdoc (~> 3.4) rdoc (~> 3.4)
thor (~> 0.14.6) thor (~> 0.14.6)
raindrops (0.8.0) raindrops (0.9.0)
rake (0.9.2.2) rake (0.9.2.2)
rdoc (3.12) rdoc (3.12)
json (~> 1.4) json (~> 1.4)
@ -283,7 +284,7 @@ GEM
rubyzip (0.9.8) rubyzip (0.9.8)
sanitize (2.0.3) sanitize (2.0.3)
nokogiri (>= 1.4.4, < 1.6) nokogiri (>= 1.4.4, < 1.6)
sass (3.1.17) sass (3.1.18)
sass-rails (3.2.5) sass-rails (3.2.5)
railties (~> 3.2.0) railties (~> 3.2.0)
sass (>= 3.1.10) sass (>= 3.1.10)
@ -300,7 +301,6 @@ GEM
hike (~> 1.2) hike (~> 1.2)
rack (~> 1.0) rack (~> 1.0)
tilt (~> 1.1, != 1.3.0) tilt (~> 1.1, != 1.3.0)
term-ansicolor (1.0.7)
thor (0.14.6) thor (0.14.6)
tilt (1.3.3) tilt (1.3.3)
treetop (1.4.10) treetop (1.4.10)
@ -315,7 +315,7 @@ GEM
rack rack
raindrops (~> 0.7) raindrops (~> 0.7)
unidecoder (1.1.1) unidecoder (1.1.1)
warden (1.1.1) warden (1.2.0)
rack (>= 1.0) rack (>= 1.0)
xpath (0.1.4) xpath (0.1.4)
nokogiri (~> 1.3) nokogiri (~> 1.3)

View File

@ -6,8 +6,6 @@ module Locomotive
before_filter :sanitize_entry_params, :only => :create before_filter :sanitize_entry_params, :only => :create
skip_before_filter :verify_authenticity_token
skip_load_and_authorize_resource skip_load_and_authorize_resource
self.responder = Locomotive::ActionController::PublicResponder # custom responder self.responder = Locomotive::ActionController::PublicResponder # custom responder
@ -17,7 +15,6 @@ module Locomotive
def create def create
@entry = @content_type.entries.create(params[:entry] || params[:content]) @entry = @content_type.entries.create(params[:entry] || params[:content])
flash[@content_type.slug.singularize] = @entry.to_presenter(:include_errors => true).as_json flash[@content_type.slug.singularize] = @entry.to_presenter(:include_errors => true).as_json
Rails.logger.debug @entry.to_presenter(:include_errors => true).as_json
respond_with @entry, :location => self.callback_url respond_with @entry, :location => self.callback_url
end end
@ -48,6 +45,13 @@ module Locomotive
end end
end end
def handle_unverified_request
if Locomotive.config.csrf_protection
reset_session
redirect_to '/', :status => 302
end
end
end end
end end
end end

View File

@ -4,6 +4,7 @@ Feature: Contact form
I want to be able to send them a message I want to be able to send them a message
Background: Background:
Given I enable the CSRF protection for public submission requests
Given I have the site: "test site" set up Given I have the site: "test site" set up
And I have a custom model named "Messages" with And I have a custom model named "Messages" with
| label | type | required | | label | type | required |
@ -16,6 +17,7 @@ Feature: Contact form
<head></head> <head></head>
<body> <body>
<form action="{{ contents.messages.public_submission_url }}" method="post"> <form action="{{ contents.messages.public_submission_url }}" method="post">
{% csrf_param %}
<input type="hidden" value="/success" name="success_callback" /> <input type="hidden" value="/success" name="success_callback" />
<input type="hidden" value="/contact" name="error_callback" /> <input type="hidden" value="/contact" name="error_callback" />
<label for="email">E-Mail Address</label> <label for="email">E-Mail Address</label>
@ -55,6 +57,20 @@ Feature: Contact form
And I press "Submit" And I press "Submit"
Then I should see "Thanks did@locomotivecms.com" Then I should see "Thanks did@locomotivecms.com"
Scenario: Can not send a message if the csrf tag is missing
Given I delete the following code "{% csrf_param %}" from the "contact" page
When I view the rendered page at "/contact"
And I press "Submit"
Then I should see "Content of the home page"
Scenario: Can send a message if the csrf protection is disabled
Given I disable the CSRF protection for public submission requests
And I view the rendered page at "/contact"
And I fill in "E-Mail Address" with "did@locomotivecms.com"
And I fill in "Message" with "LocomotiveCMS rocks"
And I press "Submit"
Then I should see "Thanks did@locomotivecms.com"
Scenario: Display errors Scenario: Display errors
When I view the rendered page at "/contact" When I view the rendered page at "/contact"
And I fill in "Message" with "LocomotiveCMS rocks" And I fill in "Message" with "LocomotiveCMS rocks"

View File

@ -38,4 +38,19 @@ end
When /^I reload the page$/ do When /^I reload the page$/ do
visit current_path visit current_path
end end
Given /^I enable the CSRF protection for public submission requests$/ do
Locomotive.config.csrf_protection = true
Locomotive::Public::ContentEntriesController.any_instance.stubs(:protect_against_forgery?).returns(true)
end
Given /^I disable the CSRF protection for public submission requests$/ do
Locomotive.config.csrf_protection = false
# pending # express the regexp above with the code you wish you had
end
Then /^it returns a (\d+) error page$/ do |code|
puts page.status_code
page.status_code.should == code.to_i
end

View File

@ -35,9 +35,15 @@ When /^I update the "([^"]*)" page with the template:$/ do |page_slug, template|
page.save! page.save!
end end
Given /^I delete the following code "([^"]*)" from the "([^"]*)" page$/ do |code, page_slug|
page = @site.pages.where(:slug => page_slug).first
page.raw_template = page.raw_template.gsub(code, '')
page.save!
end
# try to render a page by slug # try to render a page by slug
When /^I view the rendered page at "([^"]*)"$/ do |path| When /^I view the rendered page at "([^"]*)"$/ do |path|
# If we're running selenium then we need to use a differnt port # If we're running selenium then we need to use a different port
if Capybara.current_driver == :selenium if Capybara.current_driver == :selenium
visit "http://#{@site.domains.first}:#{Capybara.server_port}#{path}" visit "http://#{@site.domains.first}:#{Capybara.server_port}#{path}"
else else

View File

@ -48,6 +48,13 @@ Locomotive.configure do |config|
# add extra classes other than the defined content types among a site which will potentially used by the templatized pages. # add extra classes other than the defined content types among a site which will potentially used by the templatized pages.
# config.models_for_templatization = %w(Product) # config.models_for_templatization = %w(Product)
# "Public" forms can be protected from Cross-Site Request Forgery (CSRF) attacks.
# By default, that protection is disabled (false) in order to keep backwards compatibility with the existing public forms.
#
# Note: we strongly recommend to enable it. See the documentation about the "csrf_param" liquid tag.
#
# config.csrf_protection = true
# Rack-cache settings, mainly used for the inline resizing image module. Default options: # Rack-cache settings, mainly used for the inline resizing image module. Default options:
# config.rack_cache = { # config.rack_cache = {
# :verbose => true, # :verbose => true,

View File

@ -27,7 +27,8 @@ module Locomotive
}, },
:devise_modules => [:rememberable, :database_authenticatable, :token_authenticatable, :recoverable, :trackable, :validatable, :encryptable, { :encryptor => :sha1 }], :devise_modules => [:rememberable, :database_authenticatable, :token_authenticatable, :recoverable, :trackable, :validatable, :encryptable, { :encryptor => :sha1 }],
:context_assign_extensions => { }, :context_assign_extensions => { },
:models_for_templatization => [] :models_for_templatization => [],
:csrf_protection => false
} }
cattr_accessor :settings cattr_accessor :settings

View File

@ -0,0 +1,40 @@
module Locomotive
module Liquid
module Tags
module Csrf
class Param < ::Liquid::Tag
def render(context)
controller = context.registers[:controller]
name = controller.send(:request_forgery_protection_token).to_s
value = controller.send(:form_authenticity_token)
%(<input type="hidden" name="#{name}" value="#{value}" />)
end
end
class Meta < ::Liquid::Tag
def render(context)
controller = context.registers[:controller]
name = controller.send(:request_forgery_protection_token).to_s
value = controller.send(:form_authenticity_token)
%{
<meta name="csrf-param" content="#{name}" />
<meta name="csrf-token" content="#{value}" />
}
end
end
end
::Liquid::Template.register_tag('csrf_param', Csrf::Param)
::Liquid::Template.register_tag('csrf_meta', Csrf::Meta)
end
end
end

View File

@ -28,6 +28,13 @@ module Mongoid#:nodoc:
end end
end end
module Criterion
class Selector < Hash
# for some reason, the store method behaves differently than the []= one, causing regression bugs (query not localized)
alias :store :[]=
end
end
# without callback feature # without callback feature
module Callbacks #:nodoc: module Callbacks #:nodoc:
module ClassMethods #:nodoc: module ClassMethods #:nodoc:

View File

@ -56,7 +56,7 @@ module Locomotive
'locale' => I18n.locale, 'locale' => I18n.locale,
'default_locale' => current_site.default_locale.to_s, 'default_locale' => current_site.default_locale.to_s,
'locales' => current_site.locales, 'locales' => current_site.locales,
'current_user' => Locomotive::Liquid::Drops::CurrentUser.new(current_locomotive_account) 'current_user' => Locomotive::Liquid::Drops::CurrentUser.new(current_locomotive_account)
} }
assigns.merge!(Locomotive.config.context_assign_extensions) assigns.merge!(Locomotive.config.context_assign_extensions)

View File

@ -59,7 +59,7 @@ Gem::Specification.new do |s|
s.add_dependency 'rack-cache', '~> 1.1' s.add_dependency 'rack-cache', '~> 1.1'
s.add_dependency 'mimetype-fu', '~> 0.1.2' s.add_dependency 'mimetype-fu', '~> 0.1.2'
s.add_dependency 'multi_json', '1.3.4' s.add_dependency 'multi_json', '~> 1.3.4'
s.add_dependency 'httparty', '~> 0.8.1' s.add_dependency 'httparty', '~> 0.8.1'
s.add_dependency 'actionmailer-with-request', '~> 0.3.0' s.add_dependency 'actionmailer-with-request', '~> 0.3.0'

View File

@ -59,6 +59,13 @@ Locomotive.configure do |config|
# add extra classes other than the defined content types among a site which will potentially used by the templatized pages. # add extra classes other than the defined content types among a site which will potentially used by the templatized pages.
config.models_for_templatization = %w(Foo) config.models_for_templatization = %w(Foo)
# "Public" forms can be protected from Cross-Site Request Forgery (CSRF) attacks.
# By default, that protection is disabled (false) in order to keep backwards compatibility with the existing public forms.
#
# Note: we strongly recommend to enable it. See the documentation about the "csrf_param" liquid tag.
#
# config.csrf_protection = true
# Rack-cache settings, mainly used for the inline resizing image module. Default options: # Rack-cache settings, mainly used for the inline resizing image module. Default options:
# config.rack_cache = { # config.rack_cache = {
# :verbose => true, # :verbose => true,

View File

@ -0,0 +1,26 @@
require 'spec_helper'
describe Locomotive::Liquid::Tags::Csrf do
it 'renders the param tag for form' do
html = render_tag
html.should == '<input type="hidden" name="token" value="42" />'
end
it 'renders the meta tag used by ajax requests' do
html = render_tag('csrf_meta')
html.should include '<meta name="csrf-param" content="token" />'
html.should include '<meta name="csrf-token" content="42" />'
end
def render_tag(tag_name = 'csrf_param')
controller = mock('controller', {
:request_forgery_protection_token => 'token',
:form_authenticity_token => '42'
})
registers = { :controller => controller }
liquid_context = ::Liquid::Context.new({}, {}, registers)
Liquid::Template.parse("{% #{tag_name} %}").render(liquid_context)
end
end

View File

@ -22,6 +22,8 @@ def Locomotive.configure_for_test(force = false)
config.enable_logs = true config.enable_logs = true
config.csrf_protection = true
if force if force
Locomotive.define_subdomain_and_domains_options Locomotive.define_subdomain_and_domains_options