rack_csrf 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/LICENSE.rdoc ADDED
@@ -0,0 +1,23 @@
1
+ = LICENSE
2
+
3
+ (The MIT License)
4
+
5
+ Copyright (c) 2009 Emanuele Vicentini
6
+
7
+ Permission is hereby granted, free of charge, to any person obtaining a copy
8
+ of this software and associated documentation files (the 'Software'), to deal
9
+ in the Software without restriction, including without limitation the rights
10
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11
+ copies of the Software, and to permit persons to whom the Software is
12
+ furnished to do so, subject to the following conditions:
13
+
14
+ The above copyright notice and this permission notice shall be included in all
15
+ copies or substantial portions of the Software.
16
+
17
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23
+ SOFTWARE.
data/Manifest ADDED
@@ -0,0 +1,25 @@
1
+ cucumber.yml
2
+ example/app.rb
3
+ example/config-with-raise.ru
4
+ example/config.ru
5
+ example/views/form.erb
6
+ example/views/form_not_working.erb
7
+ example/views/response.erb
8
+ features/empty_responses.feature
9
+ features/raising_exception.feature
10
+ features/setup.feature
11
+ features/skip_some_routes.feature
12
+ features/step_definitions/request_steps.rb
13
+ features/step_definitions/response_steps.rb
14
+ features/step_definitions/setup_steps.rb
15
+ features/support/env.rb
16
+ lib/rack/csrf.rb
17
+ lib/rack/vendor/securerandom.rb
18
+ LICENSE.rdoc
19
+ Manifest
20
+ rack_csrf.gemspec
21
+ Rakefile
22
+ README.rdoc
23
+ spec/csrf_spec.rb
24
+ spec/spec.opts
25
+ spec/spec_helper.rb
data/README.rdoc ADDED
@@ -0,0 +1,69 @@
1
+ = Rack::Csrf
2
+
3
+ This is just a small Rack middleware whose only goal is to lessen the hazards
4
+ posed by CSRF attacks by trying to ensure that all requests of particular
5
+ types come from the right client, not from a mischievous impersonator.
6
+
7
+ == Usage
8
+
9
+ First of all, beyond Rack itself, there is only one prerequisite: you must set
10
+ up your rack with a session middleware, inserted anywhere before Rack::Csrf.
11
+
12
+ Every POST, PUT and DELETE request will be searched for the anti-forging
13
+ token, randomly generated by Rack::Csrf and stored inside the session. If
14
+ there's a token and it matches with the stored one, then the request is handed
15
+ over to the next rack component; if not, Rack::Csrf immediately replies with
16
+ an empty response.
17
+
18
+ I have not tested Rack::Csrf with Rack 0.4.0 or earlier versions, but it could
19
+ possibly work.
20
+
21
+ == Options
22
+
23
+ The following options allow you to tweak Rack::Csrf.
24
+
25
+ [<tt>:raise</tt>]
26
+ Set it to true to change the handling of bad request: instead of producing
27
+ an empty response, Rack::Csrf will raise an exception of class
28
+ Rack::Csrf::InvalidCsrfToken.
29
+
30
+ [<tt>:skip</tt>]
31
+ By default, Rack::Csrf checks every POST, PUT and DELETE request; passing an
32
+ array of HTTP method/URL to this option you can choose what to let pass
33
+ unchecked:
34
+
35
+ use Rack::Csrf, :skip => ['POST:/not_checking', 'PUT:/me_too']
36
+
37
+ [<tt>:field</tt>]
38
+ Default field name (see below) is <tt>_csrf</tt>; you can adapt it to
39
+ specific needs.
40
+
41
+ == Helpers
42
+
43
+ The following class methods try to ease the insertion of the anti-forging
44
+ token.
45
+
46
+ [<tt>Rack::Csrf.csrf_field</tt>]
47
+ Returns the name of the field that must be present in the request.
48
+
49
+ [<tt>Rack::Csrf.csrf_token(env)</tt>]
50
+ Given the request's environment, it generates a random token, stuffs it in
51
+ the session and returns it to the caller or simply retrieves the already
52
+ stored one.
53
+
54
+ [<tt>Rack::Csrf.csrf_tag(env)</tt>]
55
+ Given the request's environment, it generates a small HTML fragment to
56
+ insert the token in a standard form like an hidden input field with the
57
+ right value already entered for you.
58
+
59
+ == Working examples
60
+
61
+ In the +example+ directory there is a mini Sinatra application with two
62
+ slightly different rackup files. Beside Rack you only need Sinatra to try
63
+ them, but Rack::Csrf is not tailored to any particular web framework.
64
+
65
+ == Warning! Warning! Warning!
66
+
67
+ I cannot stress enough that this middleware is not a bulletproof vest or a
68
+ panacea for the CSRF plague; it is just an *aid* and by using it you cannot
69
+ forgo responsibilities for keeping your application as safe as possible.
data/Rakefile ADDED
@@ -0,0 +1,30 @@
1
+ require 'rake/clean'
2
+ require 'cucumber/rake/task'
3
+ require 'spec/rake/spectask'
4
+ require 'echoe'
5
+
6
+ Cucumber::Rake::Task.new do |c|
7
+ c.cucumber_opts = '--profile default'
8
+ end
9
+
10
+ Spec::Rake::SpecTask.new do |t|
11
+ t.spec_opts = %w(-O spec/spec.opts)
12
+ end
13
+
14
+ Echoe.new('rack_csrf', '1.0.0') do |s|
15
+ s.author = 'Emanuele Vicentini'
16
+ s.email = 'emanuele.vicentini@gmail.com'
17
+ s.summary = 'Anti-CSRF Rack middleware'
18
+ s.runtime_dependencies = ['rack >=0.9']
19
+ s.development_dependencies = ['rake >=0.8.2', 'cucumber >=1.1.13', 'rspec', 'echoe']
20
+ s.need_tar_gz = false
21
+ s.project = 'rackcsrf'
22
+ s.gemspec_format = :yaml
23
+ s.retain_gemspec = true
24
+ s.rdoc_pattern = /^README|^LICENSE/
25
+ s.url = 'http://github.com/baldowl/rack_csrf'
26
+ end
27
+
28
+ Rake::Task[:default].clear
29
+ Rake::Task.tasks.each {|t| t.clear if t.name =~ /test/}
30
+ task :default => [:features, :spec]
data/cucumber.yml ADDED
@@ -0,0 +1 @@
1
+ default: --format pretty -n features
data/example/app.rb ADDED
@@ -0,0 +1,12 @@
1
+ get '/' do
2
+ erb :form
3
+ end
4
+
5
+ get '/notworking' do
6
+ erb :form_not_working
7
+ end
8
+
9
+ post '/response' do
10
+ erb :response, :locals => {:utterance => params[:utterance],
11
+ :csrf => params[Rack::Csrf.csrf_field]}
12
+ end
@@ -0,0 +1,10 @@
1
+ require 'sinatra'
2
+ require File.dirname(__FILE__) + '/../lib/rack/csrf'
3
+
4
+ use Rack::ShowExceptions
5
+ use Rack::Session::Cookie
6
+ use Rack::Csrf, :raise => true
7
+
8
+ set :app_file, 'app.rb'
9
+
10
+ run Sinatra::Application
data/example/config.ru ADDED
@@ -0,0 +1,9 @@
1
+ require 'sinatra'
2
+ require File.dirname(__FILE__) + '/../lib/rack/csrf'
3
+
4
+ use Rack::Session::Cookie
5
+ use Rack::Csrf
6
+
7
+ set :app_file, 'app.rb'
8
+
9
+ run Sinatra::Application
@@ -0,0 +1,8 @@
1
+ <form action="/response" method="post">
2
+ <h1>Spit your utterance!</h1>
3
+ <input type="text" name="utterance">
4
+ <%= Rack::Csrf.csrf_tag(env) %>
5
+ <p><input type="submit" value="Send!"></p>
6
+ </form>
7
+
8
+ <p>Try also the <a href="/notworking">not working</a> form!</p>
@@ -0,0 +1,7 @@
1
+ <form action="/response" method="post">
2
+ <h1>Spit your utterance!</h1>
3
+ <input type="text" name="utterance">
4
+ <p><input type="submit" value="Send!"></p>
5
+ </form>
6
+
7
+ <p>Try also the <a href="/">working</a> form!</p>
@@ -0,0 +1,5 @@
1
+ <p>It seems you've just said: <em><%= utterance %></em></p>
2
+
3
+ <p>Here's the anti-CSRF token stuffed in the session: <strong><%= csrf %></strong></p>
4
+
5
+ <p><a href='/'>Back</a></p>
@@ -0,0 +1,46 @@
1
+ Feature: Handling of the HTTP requests returning an empty response
2
+
3
+ Scenario: GET request with CSRF token
4
+ Given a Rack setup with the anti-CSRF middleware
5
+ When it receives a GET request with the CSRF token
6
+ Then it lets it pass untouched
7
+
8
+ Scenario: GET request without CSRF token
9
+ Given a Rack setup with the anti-CSRF middleware
10
+ When it receives a GET request without the CSRF token
11
+ Then it lets it pass untouched
12
+
13
+ Scenario Outline: Handling request without CSRF token
14
+ Given a Rack setup with the anti-CSRF middleware
15
+ When it receives a <method> request without the CSRF token
16
+ Then it responds with 417
17
+ And the response body is empty
18
+
19
+ Examples:
20
+ | method |
21
+ | POST |
22
+ | PUT |
23
+ | DELETE |
24
+
25
+ Scenario Outline: Handling request with the right CSRF token
26
+ Given a Rack setup with the anti-CSRF middleware
27
+ When it receives a <method> request with the right CSRF token
28
+ Then it lets it pass untouched
29
+
30
+ Examples:
31
+ | method |
32
+ | POST |
33
+ | PUT |
34
+ | DELETE |
35
+
36
+ Scenario Outline: Handling request with the wrong CSRF token
37
+ Given a Rack setup with the anti-CSRF middleware
38
+ When it receives a <method> request with the wrong CSRF token
39
+ Then it responds with 417
40
+ And the response body is empty
41
+
42
+ Examples:
43
+ | method |
44
+ | POST |
45
+ | PUT |
46
+ | DELETE |
@@ -0,0 +1,41 @@
1
+ Feature: Handling of the HTTP requests raising an exception
2
+
3
+ Scenario: GET request without CSRF token
4
+ Given a Rack setup with the anti-CSRF middleware and the :raise option
5
+ When it receives a GET request without the CSRF token
6
+ Then it lets it pass untouched
7
+
8
+ Scenario Outline: Handling request without CSRF token
9
+ Given a Rack setup with the anti-CSRF middleware and the :raise option
10
+ When it receives a <method> request without the CSRF token
11
+ Then there is no response
12
+ And an exception is climbing up the stack
13
+
14
+ Examples:
15
+ | method |
16
+ | POST |
17
+ | PUT |
18
+ | DELETE |
19
+
20
+ Scenario Outline: Handling request with the right CSRF token
21
+ Given a Rack setup with the anti-CSRF middleware and the :raise option
22
+ When it receives a <method> request with the right CSRF token
23
+ Then it lets it pass untouched
24
+
25
+ Examples:
26
+ | method |
27
+ | POST |
28
+ | PUT |
29
+ | DELETE |
30
+
31
+ Scenario Outline: Handling request with the wrong CSRF token
32
+ Given a Rack setup with the anti-CSRF middleware and the :raise option
33
+ When it receives a <method> request with the wrong CSRF token
34
+ Then there is no response
35
+ And an exception is climbing up the stack
36
+
37
+ Examples:
38
+ | method |
39
+ | POST |
40
+ | PUT |
41
+ | DELETE |
@@ -0,0 +1,24 @@
1
+ Feature: Setup of the middleware
2
+
3
+ Scenario: Simple setup with session support
4
+ Given a Rack setup with the session middleware
5
+ When I insert the anti-CSRF middleware
6
+ Then I get a fully functional rack
7
+
8
+ Scenario: Simple setup without session support
9
+ Given a Rack setup without the session middleware
10
+ When I insert the anti-CSRF middleware
11
+ Then I get an error message
12
+
13
+ Scenario: Setup with :raise option
14
+ Given a Rack setup with the session middleware
15
+ When I insert the anti-CSRF middleware with the :raise option
16
+ Then I get a fully functional rack
17
+
18
+ Scenario: Setup with the :skip option
19
+ Given a Rack setup with the session middleware
20
+ When I insert the anti-CSRF middleware with the :skip option
21
+ | route |
22
+ | POST:/not_checking |
23
+ | PUT:/is_wrong |
24
+ Then I get a fully functional rack
@@ -0,0 +1,33 @@
1
+ Feature: Skipping the check for some specific routes
2
+
3
+ Scenario Outline: Skipping the check for a some requests
4
+ Given a Rack setup with the anti-CSRF middleware and the :skip option
5
+ | pair |
6
+ | POST:/not_checking |
7
+ | PUT:/is_wrong |
8
+ When it receives a <method> request for <path> without the CSRF token
9
+ Then it lets it pass untouched
10
+
11
+ Examples:
12
+ | method | path |
13
+ | POST | /not_checking |
14
+ | PUT | /is_wrong |
15
+
16
+ Scenario Outline: Keep checking the requests for other method/path pairs
17
+ Given a Rack setup with the anti-CSRF middleware and the :skip option
18
+ | pair |
19
+ | POST:/not_checking |
20
+ | PUT:/is_wrong |
21
+ When it receives a <method> request for <path> without the CSRF token
22
+ Then it responds with 417
23
+ And the response body is empty
24
+
25
+ Examples:
26
+ | method | path |
27
+ | PUT | /not_checking |
28
+ | DELETE | /not_checking |
29
+ | POST | /is_wrong |
30
+ | DELETE | /is_wrong |
31
+ | POST | / |
32
+ | PUT | /not |
33
+ | POST | /is |
@@ -0,0 +1,45 @@
1
+ When /^it receives a GET request (with|without) the CSRF token$/ do |prep|
2
+ if prep == 'with'
3
+ url = "/?#{Rack::Utils.build_query(Rack::Csrf.csrf_field => 'whatever')}"
4
+ else
5
+ url = '/'
6
+ end
7
+ @response = Rack::MockRequest.new(@app).get(url)
8
+ end
9
+
10
+ # Yes, they're not as DRY as possible, but I think they're more readable than
11
+ # a single step definition with a few captures and more complex checkings.
12
+
13
+ When /^it receives a (POST|PUT|DELETE) request without the CSRF token$/ do |http_method|
14
+ http_method.downcase!
15
+ begin
16
+ @response = Rack::MockRequest.new(@app).send http_method.to_sym, '/'
17
+ rescue Exception => e
18
+ @exception = e
19
+ end
20
+ end
21
+
22
+ When /^it receives a (POST|PUT|DELETE) request for (.+) without the CSRF token$/ do |http_method, path|
23
+ http_method.downcase!
24
+ begin
25
+ @response = Rack::MockRequest.new(@app).send http_method.to_sym, path
26
+ rescue Exception => e
27
+ @exception = e
28
+ end
29
+ end
30
+
31
+ When /^it receives a (POST|PUT|DELETE) request with the right CSRF token$/ do |http_method|
32
+ http_method.downcase!
33
+ @response = Rack::MockRequest.new(@app).send http_method.to_sym, '/',
34
+ :input => "#{Rack::Csrf.csrf_field}=right_token"
35
+ end
36
+
37
+ When /^it receives a (POST|PUT|DELETE) request with the wrong CSRF token$/ do |http_method|
38
+ http_method.downcase!
39
+ begin
40
+ @response = Rack::MockRequest.new(@app).send http_method.to_sym, '/',
41
+ :input => "#{Rack::Csrf.csrf_field}=whatever"
42
+ rescue Exception => e
43
+ @exception = e
44
+ end
45
+ end
@@ -0,0 +1,21 @@
1
+ Then /^it lets it pass untouched$/ do
2
+ @response.should be_ok
3
+ @response.should =~ /Hello world!/
4
+ end
5
+
6
+ Then /^it responds with 417$/ do
7
+ @response.status.should == 417
8
+ end
9
+
10
+ Then /^the response body is empty$/ do
11
+ @response.body.should be_empty
12
+ end
13
+
14
+ Then /^there is no response$/ do
15
+ @response.should be_nil
16
+ end
17
+
18
+ Then /^an exception is climbing up the stack$/ do
19
+ @exception.should_not be_nil
20
+ @exception.should be_an_instance_of(Rack::Csrf::InvalidCsrfToken)
21
+ end
@@ -0,0 +1,68 @@
1
+ Given /^a Rack setup (with|without) the session middleware$/ do |prep|
2
+ @rack_builder = Rack::Builder.new
3
+ @rack_builder.use Rack::Session::Cookie if prep == 'with'
4
+ end
5
+
6
+ class CsrfFaker
7
+ def initialize(app)
8
+ @app = app
9
+ end
10
+ def call(env)
11
+ env['rack.session']['rack.csrf'] = 'right_token'
12
+ @app.call(env)
13
+ end
14
+ end
15
+
16
+ # Yes, they're not as DRY as possible, but I think they're more readable than
17
+ # a single step definition with a few captures and more complex checkings.
18
+
19
+ Given /^a Rack setup with the anti\-CSRF middleware$/ do
20
+ Given 'a Rack setup with the session middleware'
21
+ @rack_builder.use CsrfFaker
22
+ When 'I insert the anti-CSRF middleware'
23
+ end
24
+
25
+ Given /^a Rack setup with the anti\-CSRF middleware and the :raise option$/ do
26
+ Given 'a Rack setup with the session middleware'
27
+ @rack_builder.use CsrfFaker
28
+ When 'I insert the anti-CSRF middleware with the :raise option'
29
+ end
30
+
31
+ Given /^a Rack setup with the anti\-CSRF middleware and the :skip option$/ do |table|
32
+ Given 'a Rack setup with the session middleware'
33
+ @rack_builder.use CsrfFaker
34
+ When 'I insert the anti-CSRF middleware with the :skip option', table
35
+ end
36
+
37
+ # Yes, they're not as DRY as possible, but I think they're more readable than
38
+ # a single step definition with a few captures and more complex checkings.
39
+
40
+ When /^I insert the anti\-CSRF middleware$/ do
41
+ @rack_builder.use Rack::Lint
42
+ @rack_builder.use Rack::Csrf
43
+ @rack_builder.run(lambda {|env| Rack::Response.new('Hello world!').finish})
44
+ @app = @rack_builder.to_app
45
+ end
46
+
47
+ When /^I insert the anti\-CSRF middleware with the :raise option$/ do
48
+ @rack_builder.use Rack::Lint
49
+ @rack_builder.use Rack::Csrf, :raise => true
50
+ @rack_builder.run(lambda {|env| Rack::Response.new('Hello world!').finish})
51
+ @app = @rack_builder.to_app
52
+ end
53
+
54
+ When /^I insert the anti\-CSRF middleware with the :skip option$/ do |table|
55
+ skippable = table.hashes.collect {|t| t.values}.flatten
56
+ @rack_builder.use Rack::Lint
57
+ @rack_builder.use Rack::Csrf, :skip => skippable
58
+ @rack_builder.run(lambda {|env| Rack::Response.new('Hello world!').finish})
59
+ @app = @rack_builder.to_app
60
+ end
61
+
62
+ Then /^I get a fully functional rack$/ do
63
+ lambda {Rack::MockRequest.new(@app).get('/')}.should_not raise_error
64
+ end
65
+
66
+ Then /^I get an error message$/ do
67
+ lambda {Rack::MockRequest.new(@app).get('/')}.should raise_error(Rack::Csrf::SessionUnavailable, 'Rack::Csrf depends on session middleware')
68
+ end
@@ -0,0 +1,4 @@
1
+ require 'rubygems'
2
+ require 'spec/expectations'
3
+
4
+ require File.dirname(__FILE__) + "/../../lib/rack/csrf"
data/lib/rack/csrf.rb ADDED
@@ -0,0 +1,57 @@
1
+ require 'rack'
2
+ begin
3
+ require 'securerandom'
4
+ rescue LoadError
5
+ require File.dirname(__FILE__) + '/vendor/securerandom'
6
+ end
7
+
8
+ module Rack
9
+ class Csrf
10
+ class SessionUnavailable < StandardError; end
11
+ class InvalidCsrfToken < StandardError; end
12
+
13
+ @@field = '_csrf'
14
+
15
+ def initialize(app, opts = {})
16
+ @app = app
17
+ @raisable = opts[:raise] || false
18
+ @skippable = opts[:skip] || []
19
+ @skippable.map {|s| s.downcase!}
20
+ @@field = opts[:field] if opts[:field]
21
+ end
22
+
23
+ def call(env)
24
+ unless env['rack.session']
25
+ raise SessionUnavailable.new('Rack::Csrf depends on session middleware')
26
+ end
27
+ req = Rack::Request.new(env)
28
+ untouchable = !%w(POST PUT DELETE).include?(req.request_method) ||
29
+ req.POST[self.class.csrf_field] == env['rack.session']['rack.csrf'] ||
30
+ skip_checking(req)
31
+ if untouchable
32
+ @app.call(env)
33
+ else
34
+ raise InvalidCsrfToken if @raisable
35
+ [417, {'Content-Type' => 'text/html', 'Content-Length' => '0'}, []]
36
+ end
37
+ end
38
+
39
+ def self.csrf_field
40
+ @@field
41
+ end
42
+
43
+ def self.csrf_token(env)
44
+ env['rack.session']['rack.csrf'] ||= SecureRandom.base64(32)
45
+ end
46
+
47
+ def self.csrf_tag(env)
48
+ %Q(<input type="hidden" name="#{csrf_field}" value="#{csrf_token(env)}" />)
49
+ end
50
+
51
+ protected
52
+
53
+ def skip_checking request
54
+ @skippable.include?(request.request_method.downcase + ':' + request.path_info)
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,256 @@
1
+ # Library taken from Ruby 1.9 SVN repository on 2009-04-15T10:25Z
2
+ # For copyright and license see http://www.ruby-lang.org
3
+
4
+ # = Secure random number generator interface.
5
+ #
6
+ # This library is an interface for secure random number generator which is
7
+ # suitable for generating session key in HTTP cookies, etc.
8
+ #
9
+ # It supports following secure random number generators.
10
+ #
11
+ # * openssl
12
+ # * /dev/urandom
13
+ # * Win32
14
+ #
15
+ # == Example
16
+ #
17
+ # # random hexadecimal string.
18
+ # p SecureRandom.hex(10) #=> "52750b30ffbc7de3b362"
19
+ # p SecureRandom.hex(10) #=> "92b15d6c8dc4beb5f559"
20
+ # p SecureRandom.hex(11) #=> "6aca1b5c58e4863e6b81b8"
21
+ # p SecureRandom.hex(12) #=> "94b2fff3e7fd9b9c391a2306"
22
+ # p SecureRandom.hex(13) #=> "39b290146bea6ce975c37cfc23"
23
+ # ...
24
+ #
25
+ # # random base64 string.
26
+ # p SecureRandom.base64(10) #=> "EcmTPZwWRAozdA=="
27
+ # p SecureRandom.base64(10) #=> "9b0nsevdwNuM/w=="
28
+ # p SecureRandom.base64(10) #=> "KO1nIU+p9DKxGg=="
29
+ # p SecureRandom.base64(11) #=> "l7XEiFja+8EKEtY="
30
+ # p SecureRandom.base64(12) #=> "7kJSM/MzBJI+75j8"
31
+ # p SecureRandom.base64(13) #=> "vKLJ0tXBHqQOuIcSIg=="
32
+ # ...
33
+ #
34
+ # # random binary string.
35
+ # p SecureRandom.random_bytes(10) #=> "\016\t{\370g\310pbr\301"
36
+ # p SecureRandom.random_bytes(10) #=> "\323U\030TO\234\357\020\a\337"
37
+ # ...
38
+
39
+ begin
40
+ require 'openssl'
41
+ rescue LoadError
42
+ end
43
+
44
+ module SecureRandom
45
+ # SecureRandom.random_bytes generates a random binary string.
46
+ #
47
+ # The argument n specifies the length of the result string.
48
+ #
49
+ # If n is not specified, 16 is assumed.
50
+ # It may be larger in future.
51
+ #
52
+ # The result may contain any byte: "\x00" - "\xff".
53
+ #
54
+ # p SecureRandom.random_bytes #=> "\xD8\\\xE0\xF4\r\xB2\xFC*WM\xFF\x83\x18\xF45\xB6"
55
+ # p SecureRandom.random_bytes #=> "m\xDC\xFC/\a\x00Uf\xB2\xB2P\xBD\xFF6S\x97"
56
+ #
57
+ # If secure random number generator is not available,
58
+ # NotImplementedError is raised.
59
+ def self.random_bytes(n=nil)
60
+ n ||= 16
61
+
62
+ if defined? OpenSSL::Random
63
+ return OpenSSL::Random.random_bytes(n)
64
+ end
65
+
66
+ if !defined?(@has_urandom) || @has_urandom
67
+ flags = File::RDONLY
68
+ flags |= File::NONBLOCK if defined? File::NONBLOCK
69
+ flags |= File::NOCTTY if defined? File::NOCTTY
70
+ flags |= File::NOFOLLOW if defined? File::NOFOLLOW
71
+ begin
72
+ File.open("/dev/urandom", flags) {|f|
73
+ unless f.stat.chardev?
74
+ raise Errno::ENOENT
75
+ end
76
+ @has_urandom = true
77
+ ret = f.readpartial(n)
78
+ if ret.length != n
79
+ raise NotImplementedError, "Unexpected partial read from random device"
80
+ end
81
+ return ret
82
+ }
83
+ rescue Errno::ENOENT
84
+ @has_urandom = false
85
+ end
86
+ end
87
+
88
+ if !defined?(@has_win32)
89
+ begin
90
+ require 'Win32API'
91
+
92
+ crypt_acquire_context = Win32API.new("advapi32", "CryptAcquireContext", 'PPPII', 'L')
93
+ @crypt_gen_random = Win32API.new("advapi32", "CryptGenRandom", 'LIP', 'L')
94
+
95
+ hProvStr = " " * 4
96
+ prov_rsa_full = 1
97
+ crypt_verifycontext = 0xF0000000
98
+
99
+ if crypt_acquire_context.call(hProvStr, nil, nil, prov_rsa_full, crypt_verifycontext) == 0
100
+ raise SystemCallError, "CryptAcquireContext failed: #{lastWin32ErrorMessage}"
101
+ end
102
+ @hProv, = hProvStr.unpack('L')
103
+
104
+ @has_win32 = true
105
+ rescue LoadError
106
+ @has_win32 = false
107
+ end
108
+ end
109
+ if @has_win32
110
+ bytes = " " * n
111
+ if @crypt_gen_random.call(@hProv, bytes.size, bytes) == 0
112
+ raise SystemCallError, "CryptGenRandom failed: #{lastWin32ErrorMessage}"
113
+ end
114
+ return bytes
115
+ end
116
+
117
+ raise NotImplementedError, "No random device"
118
+ end
119
+
120
+ # SecureRandom.hex generates a random hex string.
121
+ #
122
+ # The argument n specifies the length of the random length.
123
+ # The length of the result string is twice of n.
124
+ #
125
+ # If n is not specified, 16 is assumed.
126
+ # It may be larger in future.
127
+ #
128
+ # The result may contain 0-9 and a-f.
129
+ #
130
+ # p SecureRandom.hex #=> "eb693ec8252cd630102fd0d0fb7c3485"
131
+ # p SecureRandom.hex #=> "91dc3bfb4de5b11d029d376634589b61"
132
+ #
133
+ # If secure random number generator is not available,
134
+ # NotImplementedError is raised.
135
+ def self.hex(n=nil)
136
+ random_bytes(n).unpack("H*")[0]
137
+ end
138
+
139
+ # SecureRandom.base64 generates a random base64 string.
140
+ #
141
+ # The argument n specifies the length of the random length.
142
+ # The length of the result string is about 4/3 of n.
143
+ #
144
+ # If n is not specified, 16 is assumed.
145
+ # It may be larger in future.
146
+ #
147
+ # The result may contain A-Z, a-z, 0-9, "+", "/" and "=".
148
+ #
149
+ # p SecureRandom.base64 #=> "/2BuBuLf3+WfSKyQbRcc/A=="
150
+ # p SecureRandom.base64 #=> "6BbW0pxO0YENxn38HMUbcQ=="
151
+ #
152
+ # If secure random number generator is not available,
153
+ # NotImplementedError is raised.
154
+ #
155
+ # See RFC 3548 for base64.
156
+ def self.base64(n=nil)
157
+ [random_bytes(n)].pack("m*").delete("\n")
158
+ end
159
+
160
+ # SecureRandom.urlsafe_base64 generates a random URL-safe base64 string.
161
+ #
162
+ # The argument _n_ specifies the length of the random length.
163
+ # The length of the result string is about 4/3 of _n_.
164
+ #
165
+ # If _n_ is not specified, 16 is assumed.
166
+ # It may be larger in future.
167
+ #
168
+ # The boolean argument _padding_ specifies the padding.
169
+ # If it is false or nil, padding is not generated.
170
+ # Otherwise padding is generated.
171
+ # By default, padding is not generated because "=" may be used as a URL delimiter.
172
+ #
173
+ # The result may contain A-Z, a-z, 0-9, "-" and "_".
174
+ # "=" is also used if _padding_ is true.
175
+ #
176
+ # p SecureRandom.urlsafe_base64 #=> "b4GOKm4pOYU_-BOXcrUGDg"
177
+ # p SecureRandom.urlsafe_base64 #=> "UZLdOkzop70Ddx-IJR0ABg"
178
+ #
179
+ # p SecureRandom.urlsafe_base64(nil, true) #=> "i0XQ-7gglIsHGV2_BNPrdQ=="
180
+ # p SecureRandom.urlsafe_base64(nil, true) #=> "-M8rLhr7JEpJlqFGUMmOxg=="
181
+ #
182
+ # If secure random number generator is not available,
183
+ # NotImplementedError is raised.
184
+ #
185
+ # See RFC 3548 for URL-safe base64.
186
+ def self.urlsafe_base64(n=nil, padding=false)
187
+ s = [random_bytes(n)].pack("m*")
188
+ s.delete!("\n")
189
+ s.tr!("+/", "-_")
190
+ s.delete!("=") if !padding
191
+ s
192
+ end
193
+
194
+ # SecureRandom.random_number generates a random number.
195
+ #
196
+ # If an positive integer is given as n,
197
+ # SecureRandom.random_number returns an integer:
198
+ # 0 <= SecureRandom.random_number(n) < n.
199
+ #
200
+ # p SecureRandom.random_number(100) #=> 15
201
+ # p SecureRandom.random_number(100) #=> 88
202
+ #
203
+ # If 0 is given or an argument is not given,
204
+ # SecureRandom.random_number returns an float:
205
+ # 0.0 <= SecureRandom.random_number() < 1.0.
206
+ #
207
+ # p SecureRandom.random_number #=> 0.596506046187744
208
+ # p SecureRandom.random_number #=> 0.350621695741409
209
+ #
210
+ def self.random_number(n=0)
211
+ if 0 < n
212
+ hex = n.to_s(16)
213
+ hex = '0' + hex if (hex.length & 1) == 1
214
+ bin = [hex].pack("H*")
215
+ mask = bin[0].ord
216
+ mask |= mask >> 1
217
+ mask |= mask >> 2
218
+ mask |= mask >> 4
219
+ begin
220
+ rnd = SecureRandom.random_bytes(bin.length)
221
+ rnd[0] = (rnd[0].ord & mask).chr
222
+ end until rnd < bin
223
+ rnd.unpack("H*")[0].hex
224
+ else
225
+ # assumption: Float::MANT_DIG <= 64
226
+ i64 = SecureRandom.random_bytes(8).unpack("Q")[0]
227
+ Math.ldexp(i64 >> (64-Float::MANT_DIG), -Float::MANT_DIG)
228
+ end
229
+ end
230
+
231
+ # SecureRandom.uuid generates a v4 random UUID (Universally Unique IDentifier).
232
+ #
233
+ # p SecureRandom.uuid #=> "2d931510-d99f-494a-8c67-87feb05e1594"
234
+ # p SecureRandom.uuid #=> "62936e70-1815-439b-bf89-8492855a7e6b"
235
+ #
236
+ # See RFC 4122 for UUID.
237
+ def self.uuid
238
+ ary = self.random_bytes(16).unpack("NnnnnN")
239
+ ary[2] = (ary[2] & 0x0fff) | 0x4000
240
+ ary[3] = (ary[3] & 0x3fff) | 0x8000
241
+ "%08x-%04x-%04x-%04x-%04x%08x" % ary
242
+ end
243
+
244
+ # Following code is based on David Garamond's GUID library for Ruby.
245
+ def self.lastWin32ErrorMessage # :nodoc:
246
+ get_last_error = Win32API.new("kernel32", "GetLastError", '', 'L')
247
+ format_message = Win32API.new("kernel32", "FormatMessageA", 'LPLLPLPPPPPPPP', 'L')
248
+ format_message_ignore_inserts = 0x00000200
249
+ format_message_from_system = 0x00001000
250
+
251
+ code = get_last_error.call
252
+ msg = "\0" * 1024
253
+ len = format_message.call(format_message_ignore_inserts + format_message_from_system, 0, code, 0, msg, 1024, nil, nil, nil, nil, nil, nil, nil, nil)
254
+ msg[0, len].tr("\r", '').chomp
255
+ end
256
+ end
data/rack_csrf.gemspec ADDED
@@ -0,0 +1,131 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rack_csrf
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Emanuele Vicentini
8
+ autorequire:
9
+ bindir: bin
10
+
11
+ date: 2009-04-22 00:00:00 +02:00
12
+ default_executable:
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rack
16
+ type: :runtime
17
+ version_requirement:
18
+ version_requirements: !ruby/object:Gem::Requirement
19
+ requirements:
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: "0.9"
23
+ version:
24
+ - !ruby/object:Gem::Dependency
25
+ name: rake
26
+ type: :development
27
+ version_requirement:
28
+ version_requirements: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: 0.8.2
33
+ version:
34
+ - !ruby/object:Gem::Dependency
35
+ name: cucumber
36
+ type: :development
37
+ version_requirement:
38
+ version_requirements: !ruby/object:Gem::Requirement
39
+ requirements:
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ version: 1.1.13
43
+ version:
44
+ - !ruby/object:Gem::Dependency
45
+ name: rspec
46
+ type: :development
47
+ version_requirement:
48
+ version_requirements: !ruby/object:Gem::Requirement
49
+ requirements:
50
+ - - ">="
51
+ - !ruby/object:Gem::Version
52
+ version: "0"
53
+ version:
54
+ - !ruby/object:Gem::Dependency
55
+ name: echoe
56
+ type: :development
57
+ version_requirement:
58
+ version_requirements: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ version: "0"
63
+ version:
64
+ description: Anti-CSRF Rack middleware
65
+ email: emanuele.vicentini@gmail.com
66
+ executables: []
67
+
68
+ extensions: []
69
+
70
+ extra_rdoc_files:
71
+ - LICENSE.rdoc
72
+ - README.rdoc
73
+ files:
74
+ - cucumber.yml
75
+ - example/app.rb
76
+ - example/config-with-raise.ru
77
+ - example/config.ru
78
+ - example/views/form.erb
79
+ - example/views/form_not_working.erb
80
+ - example/views/response.erb
81
+ - features/empty_responses.feature
82
+ - features/raising_exception.feature
83
+ - features/setup.feature
84
+ - features/skip_some_routes.feature
85
+ - features/step_definitions/request_steps.rb
86
+ - features/step_definitions/response_steps.rb
87
+ - features/step_definitions/setup_steps.rb
88
+ - features/support/env.rb
89
+ - lib/rack/csrf.rb
90
+ - lib/rack/vendor/securerandom.rb
91
+ - LICENSE.rdoc
92
+ - Manifest
93
+ - rack_csrf.gemspec
94
+ - Rakefile
95
+ - README.rdoc
96
+ - spec/csrf_spec.rb
97
+ - spec/spec.opts
98
+ - spec/spec_helper.rb
99
+ has_rdoc: true
100
+ homepage: http://github.com/baldowl/rack_csrf
101
+ licenses: []
102
+
103
+ post_install_message:
104
+ rdoc_options:
105
+ - --line-numbers
106
+ - --inline-source
107
+ - --title
108
+ - Rack_csrf
109
+ - --main
110
+ - README.rdoc
111
+ require_paths:
112
+ - lib
113
+ required_ruby_version: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: "0"
118
+ version:
119
+ required_rubygems_version: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - ">="
122
+ - !ruby/object:Gem::Version
123
+ version: "1.2"
124
+ version:
125
+ requirements: []
126
+
127
+ rubyforge_project: rackcsrf
128
+ rubygems_version: 1.3.2
129
+ specification_version: 3
130
+ summary: Anti-CSRF Rack middleware
131
+ test_files: []
data/spec/csrf_spec.rb ADDED
@@ -0,0 +1,63 @@
1
+ require File.dirname(__FILE__) + '/spec_helper.rb'
2
+
3
+ describe Rack::Csrf do
4
+ describe '#csrf_field' do
5
+ it "should be '_csrf'" do
6
+ Rack::Csrf.csrf_field.should == '_csrf'
7
+ end
8
+
9
+ it "should be the value of :field option" do
10
+ fakeapp = [200, {}, []]
11
+ Rack::Csrf.new fakeapp, :field => 'whatever'
12
+ Rack::Csrf.csrf_field.should == 'whatever'
13
+ end
14
+ end
15
+
16
+ describe '#csrf_token' do
17
+ before do
18
+ @env = {'rack.session' => {}}
19
+ end
20
+
21
+ it 'should be at least 32 characters long' do
22
+ Rack::Csrf.csrf_token(@env).length.should >= 32
23
+ end
24
+
25
+ it 'should store the token inside the session if it is not already there' do
26
+ @env['rack.session'].should be_empty
27
+ Rack::Csrf.csrf_token(@env)
28
+ @env['rack.session'].should_not be_empty
29
+ @env['rack.session']['rack.csrf'].should_not be_empty
30
+ end
31
+
32
+ it 'should get the token from the session if it is already there' do
33
+ @env['rack.session'].should be_empty
34
+ csrf_token = Rack::Csrf.csrf_token(@env)
35
+ csrf_token.should == @env['rack.session']['rack.csrf']
36
+ csrf_token.should == Rack::Csrf.csrf_token(@env)
37
+ end
38
+ end
39
+
40
+ describe '#csrf_tag' do
41
+ before do
42
+ @env = {'rack.session' => {}}
43
+ @tag = Rack::Csrf.csrf_tag(@env)
44
+ end
45
+
46
+ it 'should be an input field' do
47
+ @tag.should =~ /^<input/
48
+ end
49
+
50
+ it 'should be an hidden input field' do
51
+ @tag.should =~ /type="hidden"/
52
+ end
53
+
54
+ it "should have the csrf_field's name" do
55
+ @tag.should =~ /name="#{Rack::Csrf.csrf_field}"/
56
+ end
57
+
58
+ it "should have the csrf_token's output" do
59
+ quoted_value = Regexp.quote %Q(value="#{Rack::Csrf.csrf_token(@env)}")
60
+ @tag.should =~ /#{quoted_value}/
61
+ end
62
+ end
63
+ end
data/spec/spec.opts ADDED
@@ -0,0 +1,2 @@
1
+ --colour
2
+ --format specdoc
@@ -0,0 +1,4 @@
1
+ require 'rubygems'
2
+ require 'spec'
3
+
4
+ require File.dirname(__FILE__) + '/../lib/rack/csrf'
metadata ADDED
@@ -0,0 +1,134 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rack_csrf
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Emanuele Vicentini
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-04-22 00:00:00 +02:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: rack
17
+ type: :runtime
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: "0.9"
24
+ version:
25
+ - !ruby/object:Gem::Dependency
26
+ name: rake
27
+ type: :development
28
+ version_requirement:
29
+ version_requirements: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: 0.8.2
34
+ version:
35
+ - !ruby/object:Gem::Dependency
36
+ name: cucumber
37
+ type: :development
38
+ version_requirement:
39
+ version_requirements: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ version: 1.1.13
44
+ version:
45
+ - !ruby/object:Gem::Dependency
46
+ name: rspec
47
+ type: :development
48
+ version_requirement:
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: "0"
54
+ version:
55
+ - !ruby/object:Gem::Dependency
56
+ name: echoe
57
+ type: :development
58
+ version_requirement:
59
+ version_requirements: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - ">="
62
+ - !ruby/object:Gem::Version
63
+ version: "0"
64
+ version:
65
+ description: Anti-CSRF Rack middleware
66
+ email: emanuele.vicentini@gmail.com
67
+ executables: []
68
+
69
+ extensions: []
70
+
71
+ extra_rdoc_files:
72
+ - LICENSE.rdoc
73
+ - README.rdoc
74
+ files:
75
+ - cucumber.yml
76
+ - example/app.rb
77
+ - example/config-with-raise.ru
78
+ - example/config.ru
79
+ - example/views/form.erb
80
+ - example/views/form_not_working.erb
81
+ - example/views/response.erb
82
+ - features/empty_responses.feature
83
+ - features/raising_exception.feature
84
+ - features/setup.feature
85
+ - features/skip_some_routes.feature
86
+ - features/step_definitions/request_steps.rb
87
+ - features/step_definitions/response_steps.rb
88
+ - features/step_definitions/setup_steps.rb
89
+ - features/support/env.rb
90
+ - lib/rack/csrf.rb
91
+ - lib/rack/vendor/securerandom.rb
92
+ - LICENSE.rdoc
93
+ - Manifest
94
+ - rack_csrf.gemspec
95
+ - Rakefile
96
+ - README.rdoc
97
+ - spec/csrf_spec.rb
98
+ - spec/spec.opts
99
+ - spec/spec_helper.rb
100
+ has_rdoc: true
101
+ homepage: http://github.com/baldowl/rack_csrf
102
+ licenses: []
103
+
104
+ post_install_message:
105
+ rdoc_options:
106
+ - --line-numbers
107
+ - --inline-source
108
+ - --title
109
+ - Rack_csrf
110
+ - --main
111
+ - README.rdoc
112
+ require_paths:
113
+ - lib
114
+ required_ruby_version: !ruby/object:Gem::Requirement
115
+ requirements:
116
+ - - ">="
117
+ - !ruby/object:Gem::Version
118
+ version: "0"
119
+ version:
120
+ required_rubygems_version: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: "1.2"
125
+ version:
126
+ requirements: []
127
+
128
+ rubyforge_project: rackcsrf
129
+ rubygems_version: 1.3.2
130
+ signing_key:
131
+ specification_version: 3
132
+ summary: Anti-CSRF Rack middleware
133
+ test_files: []
134
+