rack-protection 2.0.7
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.
- checksums.yaml +7 -0
- data/Gemfile +13 -0
- data/License +23 -0
- data/README.md +118 -0
- data/Rakefile +72 -0
- data/lib/rack-protection.rb +1 -0
- data/lib/rack/protection.rb +54 -0
- data/lib/rack/protection/authenticity_token.rb +196 -0
- data/lib/rack/protection/base.rb +126 -0
- data/lib/rack/protection/content_security_policy.rb +80 -0
- data/lib/rack/protection/cookie_tossing.rb +75 -0
- data/lib/rack/protection/escaped_params.rb +89 -0
- data/lib/rack/protection/form_token.rb +23 -0
- data/lib/rack/protection/frame_options.rb +37 -0
- data/lib/rack/protection/http_origin.rb +40 -0
- data/lib/rack/protection/ip_spoofing.rb +23 -0
- data/lib/rack/protection/json_csrf.rb +57 -0
- data/lib/rack/protection/path_traversal.rb +42 -0
- data/lib/rack/protection/remote_referrer.rb +20 -0
- data/lib/rack/protection/remote_token.rb +22 -0
- data/lib/rack/protection/session_hijacking.rb +36 -0
- data/lib/rack/protection/strict_transport.rb +39 -0
- data/lib/rack/protection/version.rb +5 -0
- data/lib/rack/protection/xss_header.rb +25 -0
- data/rack-protection.gemspec +40 -0
- metadata +114 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 8409e3948b276eede337038b10bb8dc59add80afbfa28fbcd2160f9f52670b82
|
4
|
+
data.tar.gz: 1d5485791b331fad229c63535c5c94761dc839194a90ee22811a9eb6a5e6be40
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 1bb5f7e556b1fdf46dd029c4f620e7062527d4b3280d0ce80e43552d5fd260f3445a4411f0f680689d215cc4532cb1e3f5b5cf64eae91a8411daccabeedcf557
|
7
|
+
data.tar.gz: 3108b981b3ada3ea6959f494f08fc6ea1c15e0bf9f269cf5d8a4c82103f9cdcf7d858fbf864eb95ab98fa9eaf041aef4d8f352d05e55167879563e7500896967
|
data/Gemfile
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
source "https://rubygems.org"
|
2
|
+
# encoding: utf-8
|
3
|
+
|
4
|
+
gem 'rake'
|
5
|
+
|
6
|
+
rack_version = ENV['rack'].to_s
|
7
|
+
rack_version = nil if rack_version.empty? or rack_version == 'stable'
|
8
|
+
rack_version = {:github => 'rack/rack'} if rack_version == 'master'
|
9
|
+
gem 'rack', rack_version
|
10
|
+
|
11
|
+
gem 'sinatra', path: '..'
|
12
|
+
|
13
|
+
gemspec
|
data/License
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2011-2017 Konstantin Haase
|
4
|
+
Copyright (c) 2015-2017 Zachary Scott
|
5
|
+
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
7
|
+
a copy of this software and associated documentation files (the
|
8
|
+
'Software'), to deal in the Software without restriction, including
|
9
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
10
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
11
|
+
permit persons to whom the Software is furnished to do so, subject to
|
12
|
+
the following conditions:
|
13
|
+
|
14
|
+
The above copyright notice and this permission notice shall be
|
15
|
+
included in all copies or substantial portions of the Software.
|
16
|
+
|
17
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
18
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
19
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
20
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
21
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
22
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
23
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,118 @@
|
|
1
|
+
# Rack::Protection
|
2
|
+
|
3
|
+
This gem protects against typical web attacks.
|
4
|
+
Should work for all Rack apps, including Rails.
|
5
|
+
|
6
|
+
# Usage
|
7
|
+
|
8
|
+
Use all protections you probably want to use:
|
9
|
+
|
10
|
+
``` ruby
|
11
|
+
# config.ru
|
12
|
+
require 'rack/protection'
|
13
|
+
use Rack::Protection
|
14
|
+
run MyApp
|
15
|
+
```
|
16
|
+
|
17
|
+
Skip a single protection middleware:
|
18
|
+
|
19
|
+
``` ruby
|
20
|
+
# config.ru
|
21
|
+
require 'rack/protection'
|
22
|
+
use Rack::Protection, :except => :path_traversal
|
23
|
+
run MyApp
|
24
|
+
```
|
25
|
+
|
26
|
+
Use a single protection middleware:
|
27
|
+
|
28
|
+
``` ruby
|
29
|
+
# config.ru
|
30
|
+
require 'rack/protection'
|
31
|
+
use Rack::Protection::AuthenticityToken
|
32
|
+
run MyApp
|
33
|
+
```
|
34
|
+
|
35
|
+
# Prevented Attacks
|
36
|
+
|
37
|
+
## Cross Site Request Forgery
|
38
|
+
|
39
|
+
Prevented by:
|
40
|
+
|
41
|
+
* [`Rack::Protection::AuthenticityToken`][authenticity-token] (not included by `use Rack::Protection`)
|
42
|
+
* [`Rack::Protection::FormToken`][form-token] (not included by `use Rack::Protection`)
|
43
|
+
* [`Rack::Protection::JsonCsrf`][json-csrf]
|
44
|
+
* [`Rack::Protection::RemoteReferrer`][remote-referrer] (not included by `use Rack::Protection`)
|
45
|
+
* [`Rack::Protection::RemoteToken`][remote-token]
|
46
|
+
* [`Rack::Protection::HttpOrigin`][http-origin]
|
47
|
+
|
48
|
+
## Cross Site Scripting
|
49
|
+
|
50
|
+
Prevented by:
|
51
|
+
|
52
|
+
* [`Rack::Protection::EscapedParams`][escaped-params] (not included by `use Rack::Protection`)
|
53
|
+
* [`Rack::Protection::XSSHeader`][xss-header] (Internet Explorer and Chrome only)
|
54
|
+
* [`Rack::Protection::ContentSecurityPolicy`][content-security-policy]
|
55
|
+
|
56
|
+
## Clickjacking
|
57
|
+
|
58
|
+
Prevented by:
|
59
|
+
|
60
|
+
* [`Rack::Protection::FrameOptions`][frame-options]
|
61
|
+
|
62
|
+
## Directory Traversal
|
63
|
+
|
64
|
+
Prevented by:
|
65
|
+
|
66
|
+
* [`Rack::Protection::PathTraversal`][path-traversal]
|
67
|
+
|
68
|
+
## Session Hijacking
|
69
|
+
|
70
|
+
Prevented by:
|
71
|
+
|
72
|
+
* [`Rack::Protection::SessionHijacking`][session-hijacking]
|
73
|
+
|
74
|
+
## Cookie Tossing
|
75
|
+
|
76
|
+
Prevented by:
|
77
|
+
* [`Rack::Protection::CookieTossing`][cookie-tossing] (not included by `use Rack::Protection`)
|
78
|
+
|
79
|
+
## IP Spoofing
|
80
|
+
|
81
|
+
Prevented by:
|
82
|
+
|
83
|
+
* [`Rack::Protection::IPSpoofing`][ip-spoofing]
|
84
|
+
|
85
|
+
## Helps to protect against protocol downgrade attacks and cookie hijacking
|
86
|
+
|
87
|
+
Prevented by:
|
88
|
+
|
89
|
+
* [`Rack::Protection::StrictTransport`][strict-transport] (not included by `use Rack::Protection`)
|
90
|
+
|
91
|
+
# Installation
|
92
|
+
|
93
|
+
gem install rack-protection
|
94
|
+
|
95
|
+
# Instrumentation
|
96
|
+
|
97
|
+
Instrumentation is enabled by passing in an instrumenter as an option.
|
98
|
+
```
|
99
|
+
use Rack::Protection, instrumenter: ActiveSupport::Notifications
|
100
|
+
```
|
101
|
+
|
102
|
+
The instrumenter is passed a namespace (String) and environment (Hash). The namespace is 'rack.protection' and the attack type can be obtained from the environment key 'rack.protection.attack'.
|
103
|
+
|
104
|
+
[authenticity-token]: http://www.sinatrarb.com/protection/authenticity_token
|
105
|
+
[content-security-policy]: http://www.sinatrarb.com/protection/content_security_policy
|
106
|
+
[cookie-tossing]: http://www.sinatrarb.com/protection/cookie_tossing
|
107
|
+
[escaped-params]: http://www.sinatrarb.com/protection/escaped_params
|
108
|
+
[form-token]: http://www.sinatrarb.com/protection/form_token
|
109
|
+
[frame-options]: http://www.sinatrarb.com/protection/frame_options
|
110
|
+
[http-origin]: http://www.sinatrarb.com/protection/http_origin
|
111
|
+
[ip-spoofing]: http://www.sinatrarb.com/protection/ip_spoofing
|
112
|
+
[json-csrf]: http://www.sinatrarb.com/protection/json_csrf
|
113
|
+
[path-traversal]: http://www.sinatrarb.com/protection/path_traversal
|
114
|
+
[remote-referrer]: http://www.sinatrarb.com/protection/remote_referrer
|
115
|
+
[remote-token]: http://www.sinatrarb.com/protection/remote_token
|
116
|
+
[session-hijacking]: http://www.sinatrarb.com/protection/session_hijacking
|
117
|
+
[strict-transport]: http://www.sinatrarb.com/protection/strict_transport
|
118
|
+
[xss-header]: http://www.sinatrarb.com/protection/xss_header
|
data/Rakefile
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
$LOAD_PATH.unshift File.expand_path('../lib', __FILE__)
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'bundler'
|
6
|
+
Bundler::GemHelper.install_tasks
|
7
|
+
rescue LoadError => e
|
8
|
+
$stderr.puts e
|
9
|
+
end
|
10
|
+
|
11
|
+
desc "run specs"
|
12
|
+
task(:spec) { ruby '-S rspec spec' }
|
13
|
+
|
14
|
+
namespace :doc do
|
15
|
+
task :readmes do
|
16
|
+
Dir.glob 'lib/rack/protection/*.rb' do |file|
|
17
|
+
excluded_files = %w[lib/rack/protection/base.rb lib/rack/protection/version.rb]
|
18
|
+
next if excluded_files.include?(file)
|
19
|
+
doc = File.read(file)[/^ module Protection(\n)+( #[^\n]*\n)*/m].scan(/^ *#(?!#) ?(.*)\n/).join("\n")
|
20
|
+
file = "doc/#{file[4..-4].tr("/_", "-")}.rdoc"
|
21
|
+
Dir.mkdir "doc" unless File.directory? "doc"
|
22
|
+
puts "writing #{file}"
|
23
|
+
File.open(file, "w") { |f| f << doc }
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
task :index do
|
28
|
+
doc = File.read("README.md")
|
29
|
+
file = "doc/rack-protection-readme.md"
|
30
|
+
Dir.mkdir "doc" unless File.directory? "doc"
|
31
|
+
puts "writing #{file}"
|
32
|
+
File.open(file, "w") { |f| f << doc }
|
33
|
+
end
|
34
|
+
|
35
|
+
task :all => [:readmes, :index]
|
36
|
+
end
|
37
|
+
|
38
|
+
desc "generate documentation"
|
39
|
+
task :doc => 'doc:all'
|
40
|
+
|
41
|
+
desc "generate gemspec"
|
42
|
+
task 'rack-protection.gemspec' do
|
43
|
+
require 'rack/protection/version'
|
44
|
+
content = File.binread 'rack-protection.gemspec'
|
45
|
+
|
46
|
+
# fetch data
|
47
|
+
fields = {
|
48
|
+
:authors => `git shortlog -sn`.force_encoding('utf-8').scan(/[^\d\s].*/),
|
49
|
+
:email => ["mail@zzak.io", "konstantin.haase@gmail.com"],
|
50
|
+
:files => %w(License README.md Rakefile Gemfile rack-protection.gemspec) + Dir['lib/**/*']
|
51
|
+
}
|
52
|
+
|
53
|
+
# insert data
|
54
|
+
fields.each do |field, values|
|
55
|
+
updated = " s.#{field} = ["
|
56
|
+
updated << values.map { |v| "\n %p" % v }.join(',')
|
57
|
+
updated << "\n ]"
|
58
|
+
content.sub!(/ s\.#{field} = \[\n( .*\n)* \]/, updated)
|
59
|
+
end
|
60
|
+
|
61
|
+
# set version
|
62
|
+
content.sub! /(s\.version.*=\s+).*/, "\\1\"#{Rack::Protection::VERSION}\""
|
63
|
+
|
64
|
+
# escape unicode
|
65
|
+
content.gsub!(/./) { |c| c.bytesize > 1 ? "\\u{#{c.codepoints.first.to_s(16)}}" : c }
|
66
|
+
|
67
|
+
File.open('rack-protection.gemspec', 'w') { |f| f << content }
|
68
|
+
end
|
69
|
+
|
70
|
+
task :gemspec => 'rack-protection.gemspec'
|
71
|
+
task :default => :spec
|
72
|
+
task :test => :spec
|
@@ -0,0 +1 @@
|
|
1
|
+
require "rack/protection"
|
@@ -0,0 +1,54 @@
|
|
1
|
+
require 'rack/protection/version'
|
2
|
+
require 'rack'
|
3
|
+
|
4
|
+
module Rack
|
5
|
+
module Protection
|
6
|
+
autoload :AuthenticityToken, 'rack/protection/authenticity_token'
|
7
|
+
autoload :Base, 'rack/protection/base'
|
8
|
+
autoload :CookieTossing, 'rack/protection/cookie_tossing'
|
9
|
+
autoload :ContentSecurityPolicy, 'rack/protection/content_security_policy'
|
10
|
+
autoload :EscapedParams, 'rack/protection/escaped_params'
|
11
|
+
autoload :FormToken, 'rack/protection/form_token'
|
12
|
+
autoload :FrameOptions, 'rack/protection/frame_options'
|
13
|
+
autoload :HttpOrigin, 'rack/protection/http_origin'
|
14
|
+
autoload :IPSpoofing, 'rack/protection/ip_spoofing'
|
15
|
+
autoload :JsonCsrf, 'rack/protection/json_csrf'
|
16
|
+
autoload :PathTraversal, 'rack/protection/path_traversal'
|
17
|
+
autoload :RemoteReferrer, 'rack/protection/remote_referrer'
|
18
|
+
autoload :RemoteToken, 'rack/protection/remote_token'
|
19
|
+
autoload :SessionHijacking, 'rack/protection/session_hijacking'
|
20
|
+
autoload :StrictTransport, 'rack/protection/strict_transport'
|
21
|
+
autoload :XSSHeader, 'rack/protection/xss_header'
|
22
|
+
|
23
|
+
def self.new(app, options = {})
|
24
|
+
# does not include: RemoteReferrer, AuthenticityToken and FormToken
|
25
|
+
except = Array options[:except]
|
26
|
+
use_these = Array options[:use]
|
27
|
+
|
28
|
+
if options.fetch(:without_session, false)
|
29
|
+
except += [:session_hijacking, :remote_token]
|
30
|
+
end
|
31
|
+
|
32
|
+
Rack::Builder.new do
|
33
|
+
# Off by default, unless added
|
34
|
+
use ::Rack::Protection::AuthenticityToken, options if use_these.include? :authenticity_token
|
35
|
+
use ::Rack::Protection::CookieTossing, options if use_these.include? :cookie_tossing
|
36
|
+
use ::Rack::Protection::ContentSecurityPolicy, options if use_these.include? :content_security_policy
|
37
|
+
use ::Rack::Protection::FormToken, options if use_these.include? :form_token
|
38
|
+
use ::Rack::Protection::RemoteReferrer, options if use_these.include? :remote_referrer
|
39
|
+
use ::Rack::Protection::StrictTransport, options if use_these.include? :strict_transport
|
40
|
+
|
41
|
+
# On by default, unless skipped
|
42
|
+
use ::Rack::Protection::FrameOptions, options unless except.include? :frame_options
|
43
|
+
use ::Rack::Protection::HttpOrigin, options unless except.include? :http_origin
|
44
|
+
use ::Rack::Protection::IPSpoofing, options unless except.include? :ip_spoofing
|
45
|
+
use ::Rack::Protection::JsonCsrf, options unless except.include? :json_csrf
|
46
|
+
use ::Rack::Protection::PathTraversal, options unless except.include? :path_traversal
|
47
|
+
use ::Rack::Protection::RemoteToken, options unless except.include? :remote_token
|
48
|
+
use ::Rack::Protection::SessionHijacking, options unless except.include? :session_hijacking
|
49
|
+
use ::Rack::Protection::XSSHeader, options unless except.include? :xss_header
|
50
|
+
run app
|
51
|
+
end.to_app
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,196 @@
|
|
1
|
+
require 'rack/protection'
|
2
|
+
require 'securerandom'
|
3
|
+
require 'base64'
|
4
|
+
|
5
|
+
module Rack
|
6
|
+
module Protection
|
7
|
+
##
|
8
|
+
# Prevented attack:: CSRF
|
9
|
+
# Supported browsers:: all
|
10
|
+
# More infos:: http://en.wikipedia.org/wiki/Cross-site_request_forgery
|
11
|
+
#
|
12
|
+
# This middleware only accepts requests other than <tt>GET</tt>,
|
13
|
+
# <tt>HEAD</tt>, <tt>OPTIONS</tt>, <tt>TRACE</tt> if their given access
|
14
|
+
# token matches the token included in the session.
|
15
|
+
#
|
16
|
+
# It checks the <tt>X-CSRF-Token</tt> header and the <tt>POST</tt> form
|
17
|
+
# data.
|
18
|
+
#
|
19
|
+
# Compatible with the {rack-csrf}[https://rubygems.org/gems/rack_csrf] gem.
|
20
|
+
#
|
21
|
+
# == Options
|
22
|
+
#
|
23
|
+
# [<tt>:authenticity_param</tt>] the name of the param that should contain
|
24
|
+
# the token on a request. Default value:
|
25
|
+
# <tt>"authenticity_token"</tt>
|
26
|
+
#
|
27
|
+
# == Example: Forms application
|
28
|
+
#
|
29
|
+
# To show what the AuthenticityToken does, this section includes a sample
|
30
|
+
# program which shows two forms. One with, and one without a CSRF token
|
31
|
+
# The one without CSRF token field will get a 403 Forbidden response.
|
32
|
+
#
|
33
|
+
# Install the gem, then run the program:
|
34
|
+
#
|
35
|
+
# gem install 'rack-protection'
|
36
|
+
# ruby server.rb
|
37
|
+
#
|
38
|
+
# Here is <tt>server.rb</tt>:
|
39
|
+
#
|
40
|
+
# require 'rack/protection'
|
41
|
+
#
|
42
|
+
# app = Rack::Builder.app do
|
43
|
+
# use Rack::Session::Cookie, secret: 'secret'
|
44
|
+
# use Rack::Protection::AuthenticityToken
|
45
|
+
#
|
46
|
+
# run -> (env) do
|
47
|
+
# [200, {}, [
|
48
|
+
# <<~EOS
|
49
|
+
# <!DOCTYPE html>
|
50
|
+
# <html lang="en">
|
51
|
+
# <head>
|
52
|
+
# <meta charset="UTF-8" />
|
53
|
+
# <title>rack-protection minimal example</title>
|
54
|
+
# </head>
|
55
|
+
# <body>
|
56
|
+
# <h1>Without Authenticity Token</h1>
|
57
|
+
# <p>This takes you to <tt>Forbidden</tt></p>
|
58
|
+
# <form action="" method="post">
|
59
|
+
# <input type="text" name="foo" />
|
60
|
+
# <input type="submit" />
|
61
|
+
# </form>
|
62
|
+
#
|
63
|
+
# <h1>With Authenticity Token</h1>
|
64
|
+
# <p>This successfully takes you to back to this form.</p>
|
65
|
+
# <form action="" method="post">
|
66
|
+
# <input type="hidden" name="authenticity_token" value="#{env['rack.session'][:csrf]}" />
|
67
|
+
# <input type="text" name="foo" />
|
68
|
+
# <input type="submit" />
|
69
|
+
# </form>
|
70
|
+
# </body>
|
71
|
+
# </html>
|
72
|
+
# EOS
|
73
|
+
# ]]
|
74
|
+
# end
|
75
|
+
# end
|
76
|
+
#
|
77
|
+
# Rack::Handler::WEBrick.run app
|
78
|
+
#
|
79
|
+
# == Example: Customize which POST parameter holds the token
|
80
|
+
#
|
81
|
+
# To customize the authenticity parameter for form data, use the
|
82
|
+
# <tt>:authenticity_param</tt> option:
|
83
|
+
# use Rack::Protection::AuthenticityToken, authenticity_param: 'your_token_param_name'
|
84
|
+
class AuthenticityToken < Base
|
85
|
+
TOKEN_LENGTH = 32
|
86
|
+
|
87
|
+
default_options :authenticity_param => 'authenticity_token',
|
88
|
+
:allow_if => nil
|
89
|
+
|
90
|
+
def self.token(session)
|
91
|
+
self.new(nil).mask_authenticity_token(session)
|
92
|
+
end
|
93
|
+
|
94
|
+
def self.random_token
|
95
|
+
SecureRandom.base64(TOKEN_LENGTH)
|
96
|
+
end
|
97
|
+
|
98
|
+
def accepts?(env)
|
99
|
+
session = session env
|
100
|
+
set_token(session)
|
101
|
+
|
102
|
+
safe?(env) ||
|
103
|
+
valid_token?(session, env['HTTP_X_CSRF_TOKEN']) ||
|
104
|
+
valid_token?(session, Request.new(env).params[options[:authenticity_param]]) ||
|
105
|
+
( options[:allow_if] && options[:allow_if].call(env) )
|
106
|
+
end
|
107
|
+
|
108
|
+
def mask_authenticity_token(session)
|
109
|
+
token = set_token(session)
|
110
|
+
mask_token(token)
|
111
|
+
end
|
112
|
+
|
113
|
+
private
|
114
|
+
|
115
|
+
def set_token(session)
|
116
|
+
session[:csrf] ||= self.class.random_token
|
117
|
+
end
|
118
|
+
|
119
|
+
# Checks the client's masked token to see if it matches the
|
120
|
+
# session token.
|
121
|
+
def valid_token?(session, token)
|
122
|
+
return false if token.nil? || token.empty?
|
123
|
+
|
124
|
+
begin
|
125
|
+
token = decode_token(token)
|
126
|
+
rescue ArgumentError # encoded_masked_token is invalid Base64
|
127
|
+
return false
|
128
|
+
end
|
129
|
+
|
130
|
+
# See if it's actually a masked token or not. We should be able
|
131
|
+
# to handle any unmasked tokens that we've issued without error.
|
132
|
+
|
133
|
+
if unmasked_token?(token)
|
134
|
+
compare_with_real_token token, session
|
135
|
+
|
136
|
+
elsif masked_token?(token)
|
137
|
+
token = unmask_token(token)
|
138
|
+
|
139
|
+
compare_with_real_token token, session
|
140
|
+
|
141
|
+
else
|
142
|
+
false # Token is malformed
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
# Creates a masked version of the authenticity token that varies
|
147
|
+
# on each request. The masking is used to mitigate SSL attacks
|
148
|
+
# like BREACH.
|
149
|
+
def mask_token(token)
|
150
|
+
token = decode_token(token)
|
151
|
+
one_time_pad = SecureRandom.random_bytes(token.length)
|
152
|
+
encrypted_token = xor_byte_strings(one_time_pad, token)
|
153
|
+
masked_token = one_time_pad + encrypted_token
|
154
|
+
encode_token(masked_token)
|
155
|
+
end
|
156
|
+
|
157
|
+
# Essentially the inverse of +mask_token+.
|
158
|
+
def unmask_token(masked_token)
|
159
|
+
# Split the token into the one-time pad and the encrypted
|
160
|
+
# value and decrypt it
|
161
|
+
token_length = masked_token.length / 2
|
162
|
+
one_time_pad = masked_token[0...token_length]
|
163
|
+
encrypted_token = masked_token[token_length..-1]
|
164
|
+
xor_byte_strings(one_time_pad, encrypted_token)
|
165
|
+
end
|
166
|
+
|
167
|
+
def unmasked_token?(token)
|
168
|
+
token.length == TOKEN_LENGTH
|
169
|
+
end
|
170
|
+
|
171
|
+
def masked_token?(token)
|
172
|
+
token.length == TOKEN_LENGTH * 2
|
173
|
+
end
|
174
|
+
|
175
|
+
def compare_with_real_token(token, session)
|
176
|
+
secure_compare(token, real_token(session))
|
177
|
+
end
|
178
|
+
|
179
|
+
def real_token(session)
|
180
|
+
decode_token(session[:csrf])
|
181
|
+
end
|
182
|
+
|
183
|
+
def encode_token(token)
|
184
|
+
Base64.strict_encode64(token)
|
185
|
+
end
|
186
|
+
|
187
|
+
def decode_token(token)
|
188
|
+
Base64.strict_decode64(token)
|
189
|
+
end
|
190
|
+
|
191
|
+
def xor_byte_strings(s1, s2)
|
192
|
+
s1.bytes.zip(s2.bytes).map { |(c1,c2)| c1 ^ c2 }.pack('c*')
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|