Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Merge pull request #4 from jeffreyiacono/master

add csrf token validation in via request header, add metatag helper
  • Loading branch information...
commit e73d62df6e90f0a144b082ba175409c6714e001a 2 parents 87f438e + cb56963
@baldowl authored
View
2  .gitignore
@@ -0,0 +1,2 @@
+Gemfile.lock
+.rvmrc
View
8 Gemfile
@@ -0,0 +1,8 @@
+source 'http://rubygems.org'
+
+gem 'rack'
+gem 'rack-test'
+gem 'rspec'
+gem 'cucumber'
+gem 'rdoc'
+gem 'jeweler'
View
44 README.rdoc
@@ -18,6 +18,8 @@ session. If there's a token and it matches with the stored one, then the
request is handed over to the next rack component; if not, Rack::Csrf
immediately replies with an empty response.
+The anti-forging token can be passed as a request parameter or a header.
+
I have not tested Rack::Csrf with Rack 0.4.0 or earlier versions, but it could
possibly work.
@@ -95,6 +97,35 @@ The following options allow you to tweak Rack::Csrf.
Default value: csrf.token
+[<tt>:header</tt>]
+ Default header name (see below) is <tt>X_CSRF_TOKEN</tt>; you can adapt it to
+ specific needs.
+
+ use Rack::Csrf, :header => 'MY_CSRF_TOKEN_HEADER'
+
+ This is useful if we want to configure our application to send the csrf
+ token in all of our ajax requests via a header. We could implement something
+ along the lines of the following:
+
+ (function(jQuery) {
+ /**
+ * Set the csrf token for each ajax operation,
+ * rack / rack_csrf handle the rest.
+ * Assumes your layout has a metatag with name of "_csrf" and you're using
+ * the default Rack:Csrf header setup.
+ */
+ jQuery.ajaxSetup({
+ beforeSend: function(xhr) {
+ var token = jQuery('meta[name="_csrf"]').attr('content');
+ xhr.setRequestHeader('X_CSRF_TOKEN', token);
+ }
+ });
+ }(jQuery));
+
+ Default value: X_CSRF_TOKEN
+
+ Note that Rack will append "HTTP_" to this value.
+
[<tt>:check_also</tt>]
By passing an array of uppercase strings to this option you can add them to
the list of HTTP methods which "mark" requests that must be searched for the
@@ -127,6 +158,9 @@ token.
[<tt>Rack::Csrf.field</tt> (also <tt>Rack::Csrf.csrf_field</tt>)]
Returns the name of the field that must be present in the request.
+[<tt>Rack::Csrf.header</tt> (also <tt>Rack::Csrf.csrf_header</tt>)]
+ Returns the name of the header that must be present in the request.
+
[<tt>Rack::Csrf.token(env)</tt> (also <tt>Rack::Csrf.csrf_token(env)</tt>)]
Given the request's environment, it generates a random token, stuffs it in
the session and returns it to the caller or simply retrieves the already
@@ -137,6 +171,16 @@ token.
insert the token in a standard form like an hidden input field with the
right value already entered for you.
+[<tt>Rack::Csrf.metatag(env, options = {})</tt> (also <tt>Rack::Csrf.csrf_metatag(env, options = {})</tt>)]
+ Given the request's environment, it generates a small HTML fragment to
+ insert the token in a standard metatag within your layout's head with the
+ right value already entered for you.
+
+ <tt>options</tt> is an optional hash that can currently take a +name+ setting, which
+ will alter the metatag's name attribute.
+
+ Default name: _csrf
+
== Working examples
In the +examples+ directory there are some small, working web applications
View
22 features/check_only_some_specific_requests.feature
@@ -9,6 +9,10 @@ Feature: Check only some specific requests
When it receives a POST request for /check/this without the CSRF token
Then it responds with 403
+ Scenario: Blocking that specific request
+ When it receives a POST request for /check/this without the CSRF header
+ Then it responds with 403
+
Scenario Outline: Not blocking other requests
When it receives a <method> request for <path> without the CSRF token
Then it lets it pass untouched
@@ -26,3 +30,21 @@ Feature: Check only some specific requests
| PATCH | /another/one |
| DELETE | /another/one |
| CUSTOM | /another/one |
+
+ Scenario Outline: Not blocking other requests
+ When it receives a <method> request for <path> without the CSRF header
+ Then it lets it pass untouched
+
+ Examples:
+ | method | path |
+ | GET | /check/this |
+ | PUT | /check/this |
+ | PATCH | /check/this |
+ | DELETE | /check/this |
+ | CUSTOM | /check/this |
+ | GET | /another/one |
+ | POST | /another/one |
+ | PUT | /another/one |
+ | PATCH | /another/one |
+ | DELETE | /another/one |
+ | CUSTOM | /another/one |
View
46 features/custom_http_methods.feature
@@ -18,6 +18,19 @@ Feature: Handling custom HTTP methods
| DELETE |
| PATCH |
+ Scenario Outline: Blocking "standard" requests without the header
+ When it receives a <method> request without the CSRF header
+ Then it responds with 403
+ And the response body is empty
+
+ Examples:
+ | method |
+ | POST |
+ | PUT |
+ | DELETE |
+ | PATCH |
+
+
Scenario Outline: Blocking "standard" requests with the wrong token
When it receives a <method> request with the wrong CSRF token
Then it responds with 403
@@ -30,6 +43,18 @@ Feature: Handling custom HTTP methods
| DELETE |
| PATCH |
+ Scenario Outline: Blocking "standard" requests with the wrong header
+ When it receives a <method> request with the wrong CSRF header
+ Then it responds with 403
+ And the response body is empty
+
+ Examples:
+ | method |
+ | POST |
+ | PUT |
+ | DELETE |
+ | PATCH |
+
Scenario Outline: Blocking requests without the token
When it receives a <method> request without the CSRF token
Then it responds with 403
@@ -40,6 +65,16 @@ Feature: Handling custom HTTP methods
| ME |
| YOU |
+ Scenario Outline: Blocking requests without the header
+ When it receives a <method> request without the CSRF header
+ Then it responds with 403
+ And the response body is empty
+
+ Examples:
+ | method |
+ | ME |
+ | YOU |
+
Scenario Outline: Blocking requests with the wrong token
When it receives a <method> request with the wrong CSRF token
Then it responds with 403
@@ -50,6 +85,17 @@ Feature: Handling custom HTTP methods
| ME |
| YOU |
+ Scenario Outline: Blocking requests with the wrong header
+ When it receives a <method> request with the wrong CSRF header
+ Then it responds with 403
+ And the response body is empty
+
+ Examples:
+ | method |
+ | ME |
+ | YOU |
+
+
Scenario Outline: Letting pass "unknown" and safe requests without the token
When it receives a <method> request without the CSRF token
Then it lets it pass untouched
View
47 features/empty_responses.feature
@@ -7,14 +7,26 @@ Feature: Handling of the HTTP requests returning an empty response
When it receives a GET request with the right CSRF token
Then it lets it pass untouched
+ Scenario: GET request with the right CSRF header
+ When it receives a GET request with the right CSRF header
+ Then it lets it pass untouched
+
Scenario: GET request with the wrong CSRF token
When it receives a GET request with the wrong CSRF token
Then it lets it pass untouched
+ Scenario: GET request with the wrong CSRF header
+ When it receives a GET request with the wrong CSRF header
+ Then it lets it pass untouched
+
Scenario: GET request without CSRF token
When it receives a GET request without the CSRF token
Then it lets it pass untouched
+ Scenario: GET request without CSRF header
+ When it receives a GET request without the CSRF header
+ Then it lets it pass untouched
+
Scenario Outline: Handling request without CSRF token
When it receives a <method> request without the CSRF token
Then it responds with 403
@@ -27,6 +39,18 @@ Feature: Handling of the HTTP requests returning an empty response
| DELETE |
| PATCH |
+ Scenario Outline: Handling request without CSRF header
+ When it receives a <method> request without the CSRF header
+ Then it responds with 403
+ And the response body is empty
+
+ Examples:
+ | method |
+ | POST |
+ | PUT |
+ | DELETE |
+ | PATCH |
+
Scenario Outline: Handling request with the right CSRF token
When it receives a <method> request with the right CSRF token
Then it lets it pass untouched
@@ -38,6 +62,17 @@ Feature: Handling of the HTTP requests returning an empty response
| DELETE |
| PATCH |
+ Scenario Outline: Handling request with the right CSRF header
+ When it receives a <method> request with the right CSRF header
+ Then it lets it pass untouched
+
+ Examples:
+ | method |
+ | POST |
+ | PUT |
+ | DELETE |
+ | PATCH |
+
Scenario Outline: Handling request with the wrong CSRF token
When it receives a <method> request with the wrong CSRF token
Then it responds with 403
@@ -49,3 +84,15 @@ Feature: Handling of the HTTP requests returning an empty response
| PUT |
| DELETE |
| PATCH |
+
+ Scenario Outline: Handling request with the wrong CSRF header
+ When it receives a <method> request with the wrong CSRF token
+ Then it responds with 403
+ And the response body is empty
+
+ Examples:
+ | method |
+ | POST |
+ | PUT |
+ | DELETE |
+ | PATCH |
View
14 features/inspecting_also_get_requests.feature
@@ -18,3 +18,17 @@ Feature: Inspecting also GET requests
When it receives a GET request without the CSRF token
Then it responds with 403
And the response body is empty
+
+ Scenario: GET request with the right CSRF header
+ When it receives a GET request with the right CSRF header
+ Then it lets it pass untouched
+
+ Scenario: GET request with the wrong CSRF header
+ When it receives a GET request with the wrong CSRF header
+ Then it responds with 403
+ And the response body is empty
+
+ Scenario: GET request without the CSRF header
+ When it receives a GET request without the CSRF header
+ Then it responds with 403
+ And the response body is empty
View
27 features/raising_exception.feature
@@ -7,6 +7,10 @@ Feature: Handling of the HTTP requests raising an exception
When it receives a GET request without the CSRF token
Then it lets it pass untouched
+ Scenario: GET request without CSRF header
+ When it receives a GET request without the CSRF header
+ Then it lets it pass untouched
+
Scenario Outline: Handling request without CSRF token
When it receives a <method> request without the CSRF token
Then there is no response
@@ -30,6 +34,17 @@ Feature: Handling of the HTTP requests raising an exception
| DELETE |
| PATCH |
+ Scenario Outline: Handling request with the right CSRF header
+ When it receives a <method> request with the right CSRF header
+ Then it lets it pass untouched
+
+ Examples:
+ | method |
+ | POST |
+ | PUT |
+ | DELETE |
+ | PATCH |
+
Scenario Outline: Handling request with the wrong CSRF token
When it receives a <method> request with the wrong CSRF token
Then there is no response
@@ -41,3 +56,15 @@ Feature: Handling of the HTTP requests raising an exception
| PUT |
| DELETE |
| PATCH |
+
+ Scenario Outline: Handling request with the wrong CSRF header
+ When it receives a <method> request with the wrong CSRF header
+ Then there is no response
+ And an exception is climbing up the stack
+
+ Examples:
+ | method |
+ | POST |
+ | PUT |
+ | DELETE |
+ | PATCH |
View
6 features/skip_if_block_passes.feature
@@ -5,7 +5,7 @@ Feature: Skipping the check if a block passes
| name | value |
| token | skip |
| User-Agent | MSIE |
- When it receives a request with headers <name> = <value> without the CSRF token
+ When it receives a request with headers <name> = <value> without the CSRF token or header
Then it lets it pass untouched
Examples:
@@ -18,7 +18,7 @@ Feature: Skipping the check if a block passes
| name | value |
| token | skip |
| User-Agent | MSIE |
- When it receives a request with headers <name> = <value> without the CSRF token
+ When it receives a request with headers <name> = <value> without the CSRF token or header
Then it responds with 403
Examples:
@@ -31,7 +31,7 @@ Feature: Skipping the check if a block passes
Given a rack with the anti-CSRF middleware and both the :skip and :skip_if options
| name | value | path |
| token | skip | POST:/ |
- When it receives a request with headers <name> = <value>, <method>, and without the CSRF token
+ When it receives a request with headers <name> = <value>, <method>, and without the CSRF token or header
Then it lets it pass untouched
Examples:
View
6 features/skip_some_routes.feature
@@ -8,7 +8,7 @@ Feature: Skipping the check for some specific routes
| POST:/not_.*\.json |
| DELETE:/cars/.*\.xml |
| PATCH:/this/one/too |
- When it receives a <method> request for <path> without the CSRF token
+ When it receives a <method> request for <path> without the CSRF token or header
Then it lets it pass untouched
Examples:
@@ -28,7 +28,7 @@ Feature: Skipping the check for some specific routes
| POST:/not_.*\.json |
| DELETE:/cars/.*\.xml |
| PATCH:/this/one/too |
- When it receives a <method> request for <path> without the CSRF token
+ When it receives a <method> request for <path> without the CSRF token or header
Then it responds with 403
And the response body is empty
@@ -63,7 +63,7 @@ Feature: Skipping the check for some specific routes
| PUT:/ |
| DELETE:/ |
| PATCH:/ |
- When it receives a <method> request with neither PATH_INFO nor CSRF token
+ When it receives a <method> request with neither PATH_INFO nor CSRF token or header
Then it lets it pass untouched
Examples:
View
34 features/step_definitions/request_steps.rb
@@ -1,7 +1,7 @@
# Yes, they're not as DRY as possible, but I think they're more readable than
# a single step definition with a few captures and more complex checkings.
-When /^it receives a (.*) request without the CSRF token$/ do |http_method|
+When /^it receives a (.*) request without the CSRF (?:token|header)$/ do |http_method|
begin
@browser.request '/', :method => http_method
rescue Exception => e
@@ -9,7 +9,7 @@
end
end
-When /^it receives a (.*) request for (.+) without the CSRF token$/ do |http_method, path|
+When /^it receives a (.*) request for (.+) without the CSRF (?:token|header|token or header)$/ do |http_method, path|
begin
@browser.request path, :method => http_method
rescue Exception => e
@@ -18,21 +18,37 @@
end
When /^it receives a (.*) request with the right CSRF token$/ do |http_method|
- @browser.request '/', :method => http_method,
- 'rack.session' => {Rack::Csrf.key => 'right_token'},
- :params => {Rack::Csrf.field => 'right_token'}
+ @browser.request '/', :method => http_method,
+ 'rack.session' => {Rack::Csrf.key => 'right_token'},
+ :params => {Rack::Csrf.field => 'right_token'}
+end
+
+When /^it receives a (.*) request with the right CSRF header$/ do |http_method|
+ @browser.request '/', :method => http_method,
+ 'rack.session' => {Rack::Csrf.key => 'right_token'},
+ Rack::Csrf.rackified_header => 'right_token'
end
When /^it receives a (.*) request with the wrong CSRF token$/ do |http_method|
begin
+ @browser.request '/', :method => http_method,
+ 'rack.session' => {Rack::Csrf.key => 'right_token'},
+ :params => {Rack::Csrf.field => 'wrong_token'}
+ rescue Exception => e
+ @exception = e
+ end
+end
+
+When /^it receives a (.*) request with the wrong CSRF header/ do |http_method|
+ begin
@browser.request '/', :method => http_method,
- :params => {Rack::Csrf.field => 'whatever'}
+ Rack::Csrf.rackified_header => 'right_token'
rescue Exception => e
@exception = e
end
end
-When /^it receives a (.*) request with neither PATH_INFO nor CSRF token$/ do |http_method|
+When /^it receives a (.*) request with neither PATH_INFO nor CSRF token or header$/ do |http_method|
begin
@browser.request '/doesntmatter', :method => http_method, 'PATH_INFO' => ''
rescue Exception => e
@@ -40,7 +56,7 @@
end
end
-When /^it receives a request with headers (.+) = ([^ ]+) without the CSRF token$/ do |name, value|
+When /^it receives a request with headers (.+) = ([^ ]+) without the CSRF token or header$/ do |name, value|
begin
@browser.request '/', Hash[:method, 'POST', name, value]
rescue Exception => e
@@ -48,7 +64,7 @@
end
end
-When /^it receives a request with headers (.+) = ([^,]+), (.+), and without the CSRF token$/ do |name, value, method|
+When /^it receives a request with headers (.+) = ([^,]+), (.+), and without the CSRF token or header$/ do |name, value, method|
begin
@browser.request '/', Hash[:method, method, name, value]
rescue Exception => e
View
11 features/step_definitions/setup_steps.rb
@@ -42,6 +42,11 @@
step 'I insert the anti-CSRF middleware with the :key option'
end
+Given /^a rack with the anti\-CSRF middleware and the :header option$/ do
+ step 'a rack with the session middleware'
+ step 'I insert the anti-CSRF middleware with the :header option'
+end
+
Given /^a rack with the anti\-CSRF middleware and the :check_also option$/ do |table|
step 'a rack with the session middleware'
step 'I insert the anti-CSRF middleware with the :check_also option', table
@@ -107,6 +112,12 @@
@browser = Rack::Test::Session.new(Rack::MockSession.new(@app))
end
+When /^I insert the anti\-CSRF middleware with the :header option$/ do
+ @rack_builder.use Rack::Csrf, :header => 'fantasy_name'
+ @app = toy_app
+ @browser = Rack::Test::Session.new(Rack::MockSession.new(@app))
+end
+
When /^I insert the anti\-CSRF middleware with the :check_also option$/ do |table|
check_also = table.hashes.collect {|t| t.values}.flatten
@rack_builder.use Rack::Csrf, :check_also => check_also
View
35 features/variation_on_header_name.feature
@@ -0,0 +1,35 @@
+Feature: Customization of the header name
+
+ Background:
+ Given a rack with the anti-CSRF middleware and the :header option
+
+ Scenario: GET request with the right CSRF header in custom field
+ When it receives a GET request with the right CSRF header
+ Then it lets it pass untouched
+
+ Scenario: GET request with the wrong CSRF header in custom field
+ When it receives a GET request with the wrong CSRF header
+ Then it lets it pass untouched
+
+ Scenario Outline: Handling request with the right CSRF header in custom field
+ When it receives a <method> request with the right CSRF header
+ Then it lets it pass untouched
+
+ Examples:
+ | method |
+ | POST |
+ | PUT |
+ | DELETE |
+ | PATCH |
+
+ Scenario Outline: Handling request with the wrong CSRF header in custom field
+ When it receives a <method> request with the wrong CSRF header
+ Then it responds with 403
+ And the response body is empty
+
+ Examples:
+ | method |
+ | POST |
+ | PUT |
+ | DELETE |
+ | PATCH |
View
36 lib/rack/csrf.rb
@@ -10,21 +10,23 @@ class Csrf
class SessionUnavailable < StandardError; end
class InvalidCsrfToken < StandardError; end
- @@field = '_csrf'
- @@key = 'csrf.token'
+ @@field = '_csrf'
+ @@header = 'X_CSRF_TOKEN'
+ @@key = 'csrf.token'
def initialize(app, opts = {})
@app = app
- @raisable = opts[:raise] || false
- @skip_list = (opts[:skip] || []).map {|r| /\A#{r}\Z/i}
- @skip_if = opts[:skip_if] if opts[:skip_if]
+ @raisable = opts[:raise] || false
+ @skip_list = (opts[:skip] || []).map {|r| /\A#{r}\Z/i}
+ @skip_if = opts[:skip_if] if opts[:skip_if]
@check_only_list = (opts[:check_only] || []).map {|r| /\A#{r}\Z/i}
- @@field = opts[:field] if opts[:field]
- @@key = opts[:key] if opts[:key]
+ @@field = opts[:field] if opts[:field]
+ @@header = opts[:header] if opts[:header]
+ @@key = opts[:key] if opts[:key]
standard_http_methods = %w(POST PUT DELETE PATCH)
- check_also = opts[:check_also] || []
+ check_also = opts[:check_also] || []
@http_methods = (standard_http_methods + check_also).flatten.uniq
end
@@ -36,7 +38,8 @@ def call(env)
req = Rack::Request.new(env)
untouchable = skip_checking(req) ||
!@http_methods.include?(req.request_method) ||
- req.params[self.class.field] == env['rack.session'][self.class.key]
+ req.params[self.class.field] == env['rack.session'][self.class.key] ||
+ req.env[self.class.rackified_header] == env['rack.session'][self.class.key]
if untouchable
@app.call(env)
else
@@ -53,6 +56,14 @@ def self.field
@@field
end
+ def self.header
+ @@header
+ end
+
+ def self.rackified_header
+ "HTTP_#{@@header.gsub('-','_').upcase}"
+ end
+
def self.token(env)
env['rack.session'][key] ||= SecureRandom.base64(32)
end
@@ -61,11 +72,18 @@ def self.tag(env)
%Q(<input type="hidden" name="#{field}" value="#{token(env)}" />)
end
+ def self.metatag(env, options = {})
+ name = options.delete(:name) || '_csrf'
+ %Q(<meta name="#{name}" content="#{token(env)}" />)
+ end
+
class << self
alias_method :csrf_key, :key
alias_method :csrf_field, :field
+ alias_method :csrf_header, :header
alias_method :csrf_token, :token
alias_method :csrf_tag, :tag
+ alias_method :csrf_metatag, :metatag
end
protected
View
62 spec/csrf_spec.rb
@@ -35,6 +35,28 @@
end
end
+ describe 'header' do
+ subject { Rack::Csrf.header }
+ it { should == 'X_CSRF_TOKEN' }
+
+ context "when set to something" do
+ before { Rack::Csrf.new nil, :header => 'something' }
+ subject { Rack::Csrf.header }
+ it { should == 'something' }
+ end
+ end
+
+ describe 'csrf_header' do
+ subject { Rack::Csrf.method(:csrf_header) }
+ it { should == Rack::Csrf.method(:header) }
+ end
+
+ describe 'rackified_header' do
+ before { Rack::Csrf.new nil, :header => 'my-header' }
+ subject { Rack::Csrf.rackified_header }
+ it { should == 'HTTP_MY_HEADER'}
+ end
+
describe 'token(env)' do
let(:env) { {'rack.session' => {}} }
@@ -109,6 +131,46 @@
end
end
+ describe 'metatag(env)' do
+ let(:env) { {'rack.session' => {}} }
+
+ context 'by default' do
+ let :metatag do
+ Rack::Csrf.new nil, :header => 'whatever'
+ Rack::Csrf.metatag env
+ end
+
+ subject { metatag }
+ it { should =~ /^<meta/ }
+ it { should =~ /name="_csrf"/ }
+ it "should have the content provided by method token(env)" do
+ quoted_value = Regexp.quote %Q(content="#{Rack::Csrf.token(env)}")
+ metatag.should =~ /#{quoted_value}/
+ end
+ end
+
+ context 'with custom name' do
+ let :metatag do
+ Rack::Csrf.new nil, :header => 'whatever'
+ Rack::Csrf.metatag env, :name => 'custom_name'
+ end
+
+ subject { metatag }
+ it { should =~ /^<meta/ }
+ it { should =~ /name="custom_name"/ }
+ it "should have the content provided by method token(env)" do
+ quoted_value = Regexp.quote %Q(content="#{Rack::Csrf.token(env)}")
+ metatag.should =~ /#{quoted_value}/
+ end
+ end
+ end
+
+ describe 'csrf_metatag(env)' do
+ it 'should be the same as method metatag(env)' do
+ Rack::Csrf.method(:csrf_metatag).should == Rack::Csrf.method(:metatag)
+ end
+ end
+
describe 'skip_checking' do
let :request do
double 'Request',
Please sign in to comment.
Something went wrong with that request. Please try again.