diff --git a/Gemfile.lock b/Gemfile.lock index 8fa7b358..5087d71e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -45,7 +45,7 @@ PATH mimetype-fu (~> 0.1.2) mongo (~> 1.5.2) mongoid (~> 2.4.9) - multi_json (= 1.3.4) + multi_json (~> 1.3.4) rack-cache (~> 1.1) rails (~> 3.2.3) rails-backbone (~> 0.6.1) @@ -109,7 +109,7 @@ GEM carrierwave-mongoid (0.1.3) carrierwave (>= 0.5.6) mongoid (~> 2.1) - cells (3.8.3) + cells (3.8.5) actionpack (~> 3.0) railties (~> 3.0) childprocess (0.3.2) @@ -123,13 +123,12 @@ GEM coffee-script (2.2.0) coffee-script-source execjs - coffee-script-source (1.3.1) - cucumber (1.1.9) + coffee-script-source (1.3.3) + cucumber (1.2.0) builder (>= 2.1.2) - diff-lcs (>= 1.1.2) - gherkin (~> 2.9.0) + diff-lcs (>= 1.1.3) + gherkin (~> 2.10.0) json (>= 1.4.6) - term-ansicolor (>= 1.0.6) cucumber-rails (1.3.0) capybara (>= 1.1.2) cucumber (>= 1.1.8) @@ -149,7 +148,7 @@ GEM ejs (1.0.0) erubis (2.7.0) excon (0.13.4) - execjs (1.3.1) + execjs (1.4.0) multi_json (~> 1.0) factory_girl (2.5.2) activesupport (>= 2.3.9) @@ -169,14 +168,14 @@ GEM net-ssh (>= 2.1.3) nokogiri (~> 1.5.0) ruby-hmac - formatador (0.2.1) + formatador (0.2.3) formtastic (2.0.2) rails (~> 3.0) fssm (0.2.9) - gherkin (2.9.3) + gherkin (2.10.0) json (>= 1.4.6) - haml (3.1.4) - highline (1.6.11) + haml (3.1.6) + highline (1.6.12) hike (1.2.1) httparty (0.8.3) multi_json (~> 1.0) @@ -186,7 +185,8 @@ GEM jquery-rails (1.0.19) railties (~> 3.0) thor (~> 0.14) - json (1.7.0) + jruby-pageant (1.0.2) + json (1.7.3) json_spec (1.0.3) multi_json (~> 1.0) rspec (~> 2.0) @@ -215,15 +215,16 @@ GEM mocha (0.9.12) mongo (1.5.2) bson (= 1.5.2) - mongoid (2.4.9) + mongoid (2.4.10) activemodel (~> 3.1) mongo (~> 1.3) tzinfo (~> 0.3.22) - multi_json (1.3.4) - multi_xml (0.4.4) + multi_json (1.3.5) + multi_xml (0.5.1) net-scp (1.0.4) net-ssh (>= 1.99.1) - net-ssh (2.3.0) + net-ssh (2.4.0) + jruby-pageant (>= 1.0.2) nokogiri (1.5.2) orm_adapter (0.0.7) pickle (0.4.10) @@ -256,7 +257,7 @@ GEM rake (>= 0.8.7) rdoc (~> 3.4) thor (~> 0.14.6) - raindrops (0.8.0) + raindrops (0.9.0) rake (0.9.2.2) rdoc (3.12) json (~> 1.4) @@ -283,7 +284,7 @@ GEM rubyzip (0.9.8) sanitize (2.0.3) nokogiri (>= 1.4.4, < 1.6) - sass (3.1.17) + sass (3.1.18) sass-rails (3.2.5) railties (~> 3.2.0) sass (>= 3.1.10) @@ -300,7 +301,6 @@ GEM hike (~> 1.2) rack (~> 1.0) tilt (~> 1.1, != 1.3.0) - term-ansicolor (1.0.7) thor (0.14.6) tilt (1.3.3) treetop (1.4.10) @@ -315,7 +315,7 @@ GEM rack raindrops (~> 0.7) unidecoder (1.1.1) - warden (1.1.1) + warden (1.2.0) rack (>= 1.0) xpath (0.1.4) nokogiri (~> 1.3) diff --git a/app/controllers/locomotive/public/content_entries_controller.rb b/app/controllers/locomotive/public/content_entries_controller.rb index 605c15a6..37dfe46f 100644 --- a/app/controllers/locomotive/public/content_entries_controller.rb +++ b/app/controllers/locomotive/public/content_entries_controller.rb @@ -6,8 +6,6 @@ module Locomotive before_filter :sanitize_entry_params, :only => :create - skip_before_filter :verify_authenticity_token - skip_load_and_authorize_resource self.responder = Locomotive::ActionController::PublicResponder # custom responder @@ -17,7 +15,6 @@ module Locomotive def create @entry = @content_type.entries.create(params[:entry] || params[:content]) 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 end @@ -48,6 +45,13 @@ module Locomotive end end + def handle_unverified_request + if Locomotive.config.csrf_protection + reset_session + redirect_to '/', :status => 302 + end + end + end end end diff --git a/features/public/contact_form.feature b/features/public/contact_form.feature index cf1a3897..0705445d 100644 --- a/features/public/contact_form.feature +++ b/features/public/contact_form.feature @@ -4,6 +4,7 @@ Feature: Contact form I want to be able to send them a message Background: + Given I enable the CSRF protection for public submission requests Given I have the site: "test site" set up And I have a custom model named "Messages" with | label | type | required | @@ -16,6 +17,7 @@ Feature: Contact form
+ {% csrf_param %} @@ -55,6 +57,20 @@ Feature: Contact form And I press "Submit" 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 When I view the rendered page at "/contact" And I fill in "Message" with "LocomotiveCMS rocks" diff --git a/features/step_definitions/more_web_steps.rb b/features/step_definitions/more_web_steps.rb index b1ea9e87..aef0cb02 100644 --- a/features/step_definitions/more_web_steps.rb +++ b/features/step_definitions/more_web_steps.rb @@ -38,4 +38,19 @@ end When /^I reload the page$/ do visit current_path -end \ No newline at end of file +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 diff --git a/features/step_definitions/page_steps.rb b/features/step_definitions/page_steps.rb index 45e5b807..f6c8c606 100644 --- a/features/step_definitions/page_steps.rb +++ b/features/step_definitions/page_steps.rb @@ -35,9 +35,15 @@ When /^I update the "([^"]*)" page with the template:$/ do |page_slug, template| page.save! 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 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 visit "http://#{@site.domains.first}:#{Capybara.server_port}#{path}" else diff --git a/lib/generators/locomotive/install/templates/locomotive.rb b/lib/generators/locomotive/install/templates/locomotive.rb index e5c4db19..e1e9b91e 100644 --- a/lib/generators/locomotive/install/templates/locomotive.rb +++ b/lib/generators/locomotive/install/templates/locomotive.rb @@ -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. # 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: # config.rack_cache = { # :verbose => true, diff --git a/lib/locomotive/configuration.rb b/lib/locomotive/configuration.rb index 1ff74325..e516142f 100644 --- a/lib/locomotive/configuration.rb +++ b/lib/locomotive/configuration.rb @@ -27,7 +27,8 @@ module Locomotive }, :devise_modules => [:rememberable, :database_authenticatable, :token_authenticatable, :recoverable, :trackable, :validatable, :encryptable, { :encryptor => :sha1 }], :context_assign_extensions => { }, - :models_for_templatization => [] + :models_for_templatization => [], + :csrf_protection => false } cattr_accessor :settings diff --git a/lib/locomotive/liquid/tags/csrf.rb b/lib/locomotive/liquid/tags/csrf.rb new file mode 100644 index 00000000..7b3d490b --- /dev/null +++ b/lib/locomotive/liquid/tags/csrf.rb @@ -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) + + %() + 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) + + %{ + + + } + end + + end + + end + + ::Liquid::Template.register_tag('csrf_param', Csrf::Param) + ::Liquid::Template.register_tag('csrf_meta', Csrf::Meta) + + end + end +end \ No newline at end of file diff --git a/lib/locomotive/mongoid/patches.rb b/lib/locomotive/mongoid/patches.rb index f0ce3fce..365b781b 100644 --- a/lib/locomotive/mongoid/patches.rb +++ b/lib/locomotive/mongoid/patches.rb @@ -28,6 +28,13 @@ module Mongoid#:nodoc: 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 module Callbacks #:nodoc: module ClassMethods #:nodoc: diff --git a/lib/locomotive/render.rb b/lib/locomotive/render.rb index 821abaaa..578cb84a 100644 --- a/lib/locomotive/render.rb +++ b/lib/locomotive/render.rb @@ -56,7 +56,7 @@ module Locomotive 'locale' => I18n.locale, 'default_locale' => current_site.default_locale.to_s, '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) diff --git a/locomotive_cms.gemspec b/locomotive_cms.gemspec index 564b6570..0f465c96 100755 --- a/locomotive_cms.gemspec +++ b/locomotive_cms.gemspec @@ -59,7 +59,7 @@ Gem::Specification.new do |s| s.add_dependency 'rack-cache', '~> 1.1' 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 'actionmailer-with-request', '~> 0.3.0' diff --git a/spec/dummy/config/initializers/locomotive.rb b/spec/dummy/config/initializers/locomotive.rb index 27e8a03a..a12c6233 100644 --- a/spec/dummy/config/initializers/locomotive.rb +++ b/spec/dummy/config/initializers/locomotive.rb @@ -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. 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: # config.rack_cache = { # :verbose => true, diff --git a/spec/lib/locomotive/liquid/tags/csrf_spec.rb b/spec/lib/locomotive/liquid/tags/csrf_spec.rb new file mode 100644 index 00000000..d49ba5b1 --- /dev/null +++ b/spec/lib/locomotive/liquid/tags/csrf_spec.rb @@ -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 == '' + end + + it 'renders the meta tag used by ajax requests' do + html = render_tag('csrf_meta') + html.should include '' + html.should include '' + 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 diff --git a/spec/support/locomotive.rb b/spec/support/locomotive.rb index 6e3be8f1..5047307b 100644 --- a/spec/support/locomotive.rb +++ b/spec/support/locomotive.rb @@ -22,6 +22,8 @@ def Locomotive.configure_for_test(force = false) config.enable_logs = true + config.csrf_protection = true + if force Locomotive.define_subdomain_and_domains_options