rack_csrf 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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
+