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