rack-protection 1.5.5 → 2.0.0.beta1
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.
Potentially problematic release.
This version of rack-protection might be problematic. Click here for more details.
- checksums.yaml +5 -5
- data/Gemfile +13 -0
- data/README.md +16 -2
- data/Rakefile +2 -5
- data/lib/rack/protection.rb +38 -24
- data/lib/rack/protection/authenticity_token.rb +104 -4
- data/lib/rack/protection/base.rb +2 -21
- data/lib/rack/protection/escaped_params.rb +2 -0
- data/lib/rack/protection/http_origin.rb +8 -0
- data/lib/rack/protection/json_csrf.rb +18 -2
- data/lib/rack/protection/path_traversal.rb +1 -4
- data/lib/rack/protection/version.rb +1 -12
- data/lib/rack/protection/xss_header.rb +1 -1
- data/rack-protection.gemspec +39 -69
- metadata +30 -61
- data/spec/authenticity_token_spec.rb +0 -48
- data/spec/base_spec.rb +0 -40
- data/spec/escaped_params_spec.rb +0 -43
- data/spec/form_token_spec.rb +0 -33
- data/spec/frame_options_spec.rb +0 -39
- data/spec/http_origin_spec.rb +0 -38
- data/spec/ip_spoofing_spec.rb +0 -35
- data/spec/json_csrf_spec.rb +0 -58
- data/spec/path_traversal_spec.rb +0 -41
- data/spec/protection_spec.rb +0 -105
- data/spec/remote_referrer_spec.rb +0 -31
- data/spec/remote_token_spec.rb +0 -42
- data/spec/session_hijacking_spec.rb +0 -55
- data/spec/spec_helper.rb +0 -163
- data/spec/xss_header_spec.rb +0 -56
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 64d5507a2ecfe3df38d6e733ba3b27ad3c9f9c4a
|
4
|
+
data.tar.gz: dd394521e2416add01c08698ecb63921a14f97b1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 48c54f8ae258c75a608fa3b5ed92be46bf24fdc9f9c91ab9bd1ce23403c6f7f2ab6203a84ca9c65a0208b4c55dea1ebb6241d7bfa277625b65604615679473b1
|
7
|
+
data.tar.gz: 05296de527c7b29c072faeaa30596fdc811f268bfe5f741fc1280593ba7022db2656efb8e288a81f405f10c2c71f82a30b5a3d50324bfa736aa886f1abc48247
|
data/Gemfile
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
source "http://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/README.md
CHANGED
@@ -1,4 +1,6 @@
|
|
1
|
-
|
1
|
+
# Rack::Protection
|
2
|
+
|
3
|
+
[](http://travis-ci.org/sinatra/rack-protection)
|
2
4
|
|
3
5
|
This gem protects against typical web attacks.
|
4
6
|
Should work for all Rack apps, including Rails.
|
@@ -50,7 +52,8 @@ Prevented by:
|
|
50
52
|
Prevented by:
|
51
53
|
|
52
54
|
* `Rack::Protection::EscapedParams` (not included by `use Rack::Protection`)
|
53
|
-
* `Rack::Protection::XSSHeader` (Internet Explorer only)
|
55
|
+
* `Rack::Protection::XSSHeader` (Internet Explorer and Chrome only)
|
56
|
+
* `Rack::Protection::ContentSecurityPolicy`
|
54
57
|
|
55
58
|
## Clickjacking
|
56
59
|
|
@@ -70,12 +73,23 @@ Prevented by:
|
|
70
73
|
|
71
74
|
* `Rack::Protection::SessionHijacking`
|
72
75
|
|
76
|
+
## Cookie Tossing
|
77
|
+
|
78
|
+
Prevented by:
|
79
|
+
* `Rack::Protection::CookieTossing` (not included by `use Rack::Protection`)
|
80
|
+
|
73
81
|
## IP Spoofing
|
74
82
|
|
75
83
|
Prevented by:
|
76
84
|
|
77
85
|
* `Rack::Protection::IPSpoofing`
|
78
86
|
|
87
|
+
## Helps to protect against protocol downgrade attacks and cookie hijacking
|
88
|
+
|
89
|
+
Prevented by:
|
90
|
+
|
91
|
+
* `Rack::Protection::StrictTransport` (not included by `use Rack::Protection`)
|
92
|
+
|
79
93
|
# Installation
|
80
94
|
|
81
95
|
gem install rack-protection
|
data/Rakefile
CHANGED
@@ -19,13 +19,10 @@ task 'rack-protection.gemspec' do
|
|
19
19
|
# fetch data
|
20
20
|
fields = {
|
21
21
|
:authors => `git shortlog -sn`.force_encoding('utf-8').scan(/[^\d\s].*/),
|
22
|
-
:email =>
|
23
|
-
:files =>
|
22
|
+
:email => ["mail@zzak.io", "konstantin.haase@gmail.com"],
|
23
|
+
:files => %w(License README.md Rakefile Gemfile rack-protection.gemspec) + Dir['lib/**/*']
|
24
24
|
}
|
25
25
|
|
26
|
-
# double email :(
|
27
|
-
fields[:email].delete("konstantin.haase@gmail.com")
|
28
|
-
|
29
26
|
# insert data
|
30
27
|
fields.each do |field, values|
|
31
28
|
updated = " s.#{field} = ["
|
data/lib/rack/protection.rb
CHANGED
@@ -3,36 +3,50 @@ require 'rack'
|
|
3
3
|
|
4
4
|
module Rack
|
5
5
|
module Protection
|
6
|
-
autoload :AuthenticityToken,
|
7
|
-
autoload :Base,
|
8
|
-
autoload :
|
9
|
-
autoload :
|
10
|
-
autoload :
|
11
|
-
autoload :
|
12
|
-
autoload :
|
13
|
-
autoload :
|
14
|
-
autoload :
|
15
|
-
autoload :
|
16
|
-
autoload :
|
17
|
-
autoload :
|
18
|
-
autoload :
|
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'
|
19
22
|
|
20
23
|
def self.new(app, options = {})
|
21
24
|
# does not include: RemoteReferrer, AuthenticityToken and FormToken
|
22
25
|
except = Array options[:except]
|
23
26
|
use_these = Array options[:use]
|
27
|
+
|
28
|
+
if options.fetch(:without_session, false)
|
29
|
+
except += [:session_hijacking, :remote_token]
|
30
|
+
end
|
31
|
+
|
24
32
|
Rack::Builder.new do
|
25
|
-
|
26
|
-
use ::Rack::Protection::AuthenticityToken,options if use_these.include? :authenticity_token
|
27
|
-
use ::Rack::Protection::
|
28
|
-
use ::Rack::Protection::
|
29
|
-
use ::Rack::Protection::
|
30
|
-
use ::Rack::Protection::
|
31
|
-
use ::Rack::Protection::
|
32
|
-
|
33
|
-
|
34
|
-
use ::Rack::Protection::
|
35
|
-
use ::Rack::Protection::
|
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
|
36
50
|
run app
|
37
51
|
end.to_app
|
38
52
|
end
|
@@ -1,4 +1,6 @@
|
|
1
1
|
require 'rack/protection'
|
2
|
+
require 'securerandom'
|
3
|
+
require 'base64'
|
2
4
|
|
3
5
|
module Rack
|
4
6
|
module Protection
|
@@ -17,14 +19,112 @@ module Rack
|
|
17
19
|
# authenticity_param: Defines the param's name that should contain the token on a request.
|
18
20
|
#
|
19
21
|
class AuthenticityToken < Base
|
20
|
-
default_options :authenticity_param => 'authenticity_token'
|
22
|
+
default_options :authenticity_param => 'authenticity_token',
|
23
|
+
:authenticity_token_length => 32,
|
24
|
+
:allow_if => nil
|
25
|
+
|
26
|
+
class << self
|
27
|
+
def token(session)
|
28
|
+
mask_token(session[:csrf])
|
29
|
+
end
|
30
|
+
|
31
|
+
def random_token(length = 32)
|
32
|
+
SecureRandom.base64(length)
|
33
|
+
end
|
34
|
+
|
35
|
+
# Creates a masked version of the authenticity token that varies
|
36
|
+
# on each request. The masking is used to mitigate SSL attacks
|
37
|
+
# like BREACH.
|
38
|
+
def mask_token(token)
|
39
|
+
token = decode_token(token)
|
40
|
+
one_time_pad = SecureRandom.random_bytes(token.length)
|
41
|
+
encrypted_token = xor_byte_strings(one_time_pad, token)
|
42
|
+
masked_token = one_time_pad + encrypted_token
|
43
|
+
encode_token masked_token
|
44
|
+
end
|
45
|
+
|
46
|
+
# Essentially the inverse of +mask_token+.
|
47
|
+
def unmask_decoded_token(masked_token)
|
48
|
+
# Split the token into the one-time pad and the encrypted
|
49
|
+
# value and decrypt it
|
50
|
+
token_length = masked_token.length / 2
|
51
|
+
one_time_pad = masked_token[0...token_length]
|
52
|
+
encrypted_token = masked_token[token_length..-1]
|
53
|
+
xor_byte_strings(one_time_pad, encrypted_token)
|
54
|
+
end
|
55
|
+
|
56
|
+
def encode_token(token)
|
57
|
+
Base64.strict_encode64(token)
|
58
|
+
end
|
59
|
+
|
60
|
+
def decode_token(token)
|
61
|
+
Base64.strict_decode64(token)
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
def xor_byte_strings(s1, s2)
|
67
|
+
s1.bytes.zip(s2.bytes).map { |(c1,c2)| c1 ^ c2 }.pack('c*')
|
68
|
+
end
|
69
|
+
end
|
21
70
|
|
22
71
|
def accepts?(env)
|
23
72
|
session = session env
|
24
|
-
|
73
|
+
session[:csrf] ||= self.class.random_token(token_length)
|
74
|
+
|
25
75
|
safe?(env) ||
|
26
|
-
|
27
|
-
|
76
|
+
valid_token?(session, env['HTTP_X_CSRF_TOKEN']) ||
|
77
|
+
valid_token?(session, Request.new(env).params[options[:authenticity_param]]) ||
|
78
|
+
( options[:allow_if] && options[:allow_if].call(env) )
|
79
|
+
end
|
80
|
+
|
81
|
+
private
|
82
|
+
|
83
|
+
def token_length
|
84
|
+
options[:authenticity_token_length]
|
85
|
+
end
|
86
|
+
|
87
|
+
# Checks the client's masked token to see if it matches the
|
88
|
+
# session token.
|
89
|
+
def valid_token?(session, token)
|
90
|
+
return false if token.nil? || token.empty?
|
91
|
+
|
92
|
+
begin
|
93
|
+
token = self.class.decode_token(token)
|
94
|
+
rescue ArgumentError # encoded_masked_token is invalid Base64
|
95
|
+
return false
|
96
|
+
end
|
97
|
+
|
98
|
+
# See if it's actually a masked token or not. We should be able
|
99
|
+
# to handle any unmasked tokens that we've issued without error.
|
100
|
+
|
101
|
+
if unmasked_token?(token)
|
102
|
+
compare_with_real_token token, session
|
103
|
+
|
104
|
+
elsif masked_token?(token)
|
105
|
+
token = self.class.unmask_decoded_token(token)
|
106
|
+
|
107
|
+
compare_with_real_token token, session
|
108
|
+
|
109
|
+
else
|
110
|
+
false # Token is malformed
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def unmasked_token?(token)
|
115
|
+
token.length == token_length
|
116
|
+
end
|
117
|
+
|
118
|
+
def masked_token?(token)
|
119
|
+
token.length == token_length * 2
|
120
|
+
end
|
121
|
+
|
122
|
+
def compare_with_real_token(token, session)
|
123
|
+
secure_compare(token, real_token(session))
|
124
|
+
end
|
125
|
+
|
126
|
+
def real_token(session)
|
127
|
+
self.class.decode_token(session[:csrf])
|
28
128
|
end
|
29
129
|
end
|
30
130
|
end
|
data/lib/rack/protection/base.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
require 'rack/protection'
|
2
|
+
require 'rack/utils'
|
2
3
|
require 'digest'
|
3
4
|
require 'logger'
|
4
5
|
require 'uri'
|
@@ -110,28 +111,8 @@ module Rack
|
|
110
111
|
options[:encryptor].hexdigest value.to_s
|
111
112
|
end
|
112
113
|
|
113
|
-
# The implementations of secure_compare and bytesize are taken from
|
114
|
-
# Rack::Utils to be able to support rack older than XXXX.
|
115
114
|
def secure_compare(a, b)
|
116
|
-
|
117
|
-
|
118
|
-
l = a.unpack("C*")
|
119
|
-
|
120
|
-
r, i = 0, -1
|
121
|
-
b.each_byte { |v| r |= v ^ l[i+=1] }
|
122
|
-
r == 0
|
123
|
-
end
|
124
|
-
|
125
|
-
# Return the bytesize of String; uses String#size under Ruby 1.8 and
|
126
|
-
# String#bytesize under 1.9.
|
127
|
-
if ''.respond_to?(:bytesize)
|
128
|
-
def bytesize(string)
|
129
|
-
string.bytesize
|
130
|
-
end
|
131
|
-
else
|
132
|
-
def bytesize(string)
|
133
|
-
string.size
|
134
|
-
end
|
115
|
+
Rack::Utils.secure_compare(a.to_s, b.to_s)
|
135
116
|
end
|
136
117
|
|
137
118
|
alias default_reaction deny
|
@@ -1,5 +1,6 @@
|
|
1
1
|
require 'rack/protection'
|
2
2
|
require 'rack/utils'
|
3
|
+
require 'tempfile'
|
3
4
|
|
4
5
|
begin
|
5
6
|
require 'escape_utils'
|
@@ -66,6 +67,7 @@ module Rack
|
|
66
67
|
when Hash then escape_hash(object)
|
67
68
|
when Array then object.map { |o| escape(o) }
|
68
69
|
when String then escape_string(object)
|
70
|
+
when Tempfile then object
|
69
71
|
else nil
|
70
72
|
end
|
71
73
|
end
|
@@ -10,9 +10,16 @@ module Rack
|
|
10
10
|
#
|
11
11
|
# Does not accept unsafe HTTP requests when value of Origin HTTP request header
|
12
12
|
# does not match default or whitelisted URIs.
|
13
|
+
#
|
14
|
+
# If you want to whitelist a specific domain, you can pass in as the `:origin_whitelist` option:
|
15
|
+
#
|
16
|
+
# use Rack::Protection, origin_whitelist: ["http://localhost:3000", "http://127.0.01:3000"]
|
17
|
+
#
|
18
|
+
# The `:allow_if` option can also be set to a proc to use custom allow/deny logic.
|
13
19
|
class HttpOrigin < Base
|
14
20
|
DEFAULT_PORTS = { 'http' => 80, 'https' => 443, 'coffee' => 80 }
|
15
21
|
default_reaction :deny
|
22
|
+
default_options :allow_if => nil
|
16
23
|
|
17
24
|
def base_url(env)
|
18
25
|
request = Rack::Request.new(env)
|
@@ -24,6 +31,7 @@ module Rack
|
|
24
31
|
return true if safe? env
|
25
32
|
return true unless origin = env['HTTP_ORIGIN']
|
26
33
|
return true if base_url(env) == origin
|
34
|
+
return true if options[:allow_if] && options[:allow_if].call(env)
|
27
35
|
Array(options[:origin_whitelist]).include? origin
|
28
36
|
end
|
29
37
|
|
@@ -10,6 +10,9 @@ module Rack
|
|
10
10
|
# JSON GET APIs are vulnerable to being embedded as JavaScript while the
|
11
11
|
# Array prototype has been patched to track data. Checks the referrer
|
12
12
|
# even on GET requests if the content type is JSON.
|
13
|
+
#
|
14
|
+
# Uses HttpOrigin to determine if requests are safe, please refer to the
|
15
|
+
# documentation for more.
|
13
16
|
class JsonCsrf < Base
|
14
17
|
alias react deny
|
15
18
|
|
@@ -17,9 +20,10 @@ module Rack
|
|
17
20
|
request = Request.new(env)
|
18
21
|
status, headers, body = app.call(env)
|
19
22
|
|
20
|
-
if has_vector?
|
23
|
+
if has_vector?(request, headers)
|
21
24
|
warn env, "attack prevented by #{self.class}"
|
22
|
-
|
25
|
+
|
26
|
+
react_and_close(env, body) or [status, headers, body]
|
23
27
|
else
|
24
28
|
[status, headers, body]
|
25
29
|
end
|
@@ -30,6 +34,18 @@ module Rack
|
|
30
34
|
return false unless headers['Content-Type'].to_s.split(';', 2).first =~ /^\s*application\/json\s*$/
|
31
35
|
origin(request.env).nil? and referrer(request.env) != request.host
|
32
36
|
end
|
37
|
+
|
38
|
+
def react_and_close(env, body)
|
39
|
+
reaction = react(env)
|
40
|
+
|
41
|
+
close_body(body) if reaction
|
42
|
+
|
43
|
+
reaction
|
44
|
+
end
|
45
|
+
|
46
|
+
def close_body(body)
|
47
|
+
body.close if body.respond_to?(:close)
|
48
|
+
end
|
33
49
|
end
|
34
50
|
end
|
35
51
|
end
|
@@ -24,17 +24,14 @@ module Rack
|
|
24
24
|
encoding = path.encoding
|
25
25
|
dot = '.'.encode(encoding)
|
26
26
|
slash = '/'.encode(encoding)
|
27
|
-
backslash = '\\'.encode(encoding)
|
28
27
|
else
|
29
28
|
# Ruby 1.8
|
30
29
|
dot = '.'
|
31
30
|
slash = '/'
|
32
|
-
backslash = '\\'
|
33
31
|
end
|
34
32
|
|
35
33
|
parts = []
|
36
|
-
unescaped = path.gsub(/%2e/i, dot).gsub(/%2f/i, slash)
|
37
|
-
unescaped = unescaped.gsub(backslash, slash)
|
34
|
+
unescaped = path.gsub(/%2e/i, dot).gsub(/%2f/i, slash)
|
38
35
|
|
39
36
|
unescaped.split(slash).each do |part|
|
40
37
|
next if part.empty? or part == dot
|
@@ -1,16 +1,5 @@
|
|
1
1
|
module Rack
|
2
2
|
module Protection
|
3
|
-
|
4
|
-
VERSION
|
5
|
-
end
|
6
|
-
|
7
|
-
SIGNATURE = [1, 5, 5]
|
8
|
-
VERSION = SIGNATURE.join('.')
|
9
|
-
|
10
|
-
VERSION.extend Comparable
|
11
|
-
def VERSION.<=>(other)
|
12
|
-
other = other.split('.').map { |i| i.to_i } if other.respond_to? :split
|
13
|
-
SIGNATURE <=> Array(other)
|
14
|
-
end
|
3
|
+
VERSION = ::Sinatra::VERSION
|
15
4
|
end
|
16
5
|
end
|
@@ -4,7 +4,7 @@ module Rack
|
|
4
4
|
module Protection
|
5
5
|
##
|
6
6
|
# Prevented attack:: Non-permanent XSS
|
7
|
-
# Supported browsers:: Internet Explorer 8 and
|
7
|
+
# Supported browsers:: Internet Explorer 8+ and Chrome
|
8
8
|
# More infos:: http://blogs.msdn.com/b/ie/archive/2008/07/01/ie8-security-part-iv-the-xss-filter.aspx
|
9
9
|
#
|
10
10
|
# Sets X-XSS-Protection header to tell the browser to block attacks.
|