rack-protection 1.3.2 → 1.5.5
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/README.md +9 -1
- data/Rakefile +7 -4
- data/lib/rack/protection.rb +4 -0
- data/lib/rack/protection/authenticity_token.rb +10 -3
- data/lib/rack/protection/base.rb +40 -2
- data/lib/rack/protection/frame_options.rb +10 -4
- data/lib/rack/protection/json_csrf.rb +14 -7
- data/lib/rack/protection/path_traversal.rb +19 -5
- data/lib/rack/protection/remote_referrer.rb +0 -3
- data/lib/rack/protection/session_hijacking.rb +3 -3
- data/lib/rack/protection/version.rb +1 -1
- data/lib/rack/protection/xss_header.rb +2 -10
- data/rack-protection.gemspec +41 -13
- data/spec/authenticity_token_spec.rb +15 -0
- data/spec/base_spec.rb +40 -0
- data/spec/escaped_params_spec.rb +0 -1
- data/spec/json_csrf_spec.rb +18 -0
- data/spec/path_traversal_spec.rb +15 -2
- data/spec/protection_spec.rb +68 -0
- data/spec/session_hijacking_spec.rb +3 -2
- data/spec/xss_header_spec.rb +6 -0
- metadata +52 -33
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 2b7d78da301d9f7fc81ae73e46a389c2b8ce10ab8121f169fd760018ac506d47
|
4
|
+
data.tar.gz: a91bd28f8624f325d6714262ac11d83e0a347405f14e600780cb1bfd846e5b34
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 0c5de92c0283313c00d50c1f9a219c808ad587caabff81c4d1530abd8f0e7d9c0f3753ad9bab7c06a29ce97b2a717fddc04ced642adff058f3431419286e4da6
|
7
|
+
data.tar.gz: d3bf5830bf30475871b73ba54ee38f962bd93c2e1f420b59b649d6dcb7f97d89f091d3add3f692ba847ffe5f5c6cade665d522c86220087d977fb3706e41bd58
|
data/README.md
CHANGED
@@ -50,7 +50,7 @@ Prevented by:
|
|
50
50
|
Prevented by:
|
51
51
|
|
52
52
|
* `Rack::Protection::EscapedParams` (not included by `use Rack::Protection`)
|
53
|
-
* `Rack::Protection::
|
53
|
+
* `Rack::Protection::XSSHeader` (Internet Explorer only)
|
54
54
|
|
55
55
|
## Clickjacking
|
56
56
|
|
@@ -80,3 +80,11 @@ Prevented by:
|
|
80
80
|
|
81
81
|
gem install rack-protection
|
82
82
|
|
83
|
+
# Instrumentation
|
84
|
+
|
85
|
+
Instrumentation is enabled by passing in an instrumenter as an option.
|
86
|
+
```
|
87
|
+
use Rack::Protection, instrumenter: ActiveSupport::Notifications
|
88
|
+
```
|
89
|
+
|
90
|
+
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'.
|
data/Rakefile
CHANGED
@@ -14,15 +14,18 @@ task(:spec) { ruby '-S rspec spec' }
|
|
14
14
|
desc "generate gemspec"
|
15
15
|
task 'rack-protection.gemspec' do
|
16
16
|
require 'rack/protection/version'
|
17
|
-
content = File.
|
17
|
+
content = File.binread 'rack-protection.gemspec'
|
18
18
|
|
19
19
|
# fetch data
|
20
20
|
fields = {
|
21
|
-
:authors => `git shortlog -sn`.scan(/[^\d\s].*/),
|
22
|
-
:email => `git shortlog -sne`.scan(/[^<]+@[^>]+/),
|
23
|
-
:files => `git ls-files`.split("\n").reject { |f| f =~ /^(\.|Gemfile)/ }
|
21
|
+
:authors => `git shortlog -sn`.force_encoding('utf-8').scan(/[^\d\s].*/),
|
22
|
+
:email => `git shortlog -sne`.force_encoding('utf-8').scan(/[^<]+@[^>]+/),
|
23
|
+
:files => `git ls-files`.force_encoding('utf-8').split("\n").reject { |f| f =~ /^(\.|Gemfile)/ }
|
24
24
|
}
|
25
25
|
|
26
|
+
# double email :(
|
27
|
+
fields[:email].delete("konstantin.haase@gmail.com")
|
28
|
+
|
26
29
|
# insert data
|
27
30
|
fields.each do |field, values|
|
28
31
|
updated = " s.#{field} = ["
|
data/lib/rack/protection.rb
CHANGED
@@ -20,7 +20,11 @@ module Rack
|
|
20
20
|
def self.new(app, options = {})
|
21
21
|
# does not include: RemoteReferrer, AuthenticityToken and FormToken
|
22
22
|
except = Array options[:except]
|
23
|
+
use_these = Array options[:use]
|
23
24
|
Rack::Builder.new do
|
25
|
+
use ::Rack::Protection::RemoteReferrer, options if use_these.include? :remote_referrer
|
26
|
+
use ::Rack::Protection::AuthenticityToken,options if use_these.include? :authenticity_token
|
27
|
+
use ::Rack::Protection::FormToken, options if use_these.include? :form_token
|
24
28
|
use ::Rack::Protection::FrameOptions, options unless except.include? :frame_options
|
25
29
|
use ::Rack::Protection::HttpOrigin, options unless except.include? :http_origin
|
26
30
|
use ::Rack::Protection::IPSpoofing, options unless except.include? :ip_spoofing
|
@@ -11,13 +11,20 @@ module Rack
|
|
11
11
|
# included in the session.
|
12
12
|
#
|
13
13
|
# Compatible with Rails and rack-csrf.
|
14
|
+
#
|
15
|
+
# Options:
|
16
|
+
#
|
17
|
+
# authenticity_param: Defines the param's name that should contain the token on a request.
|
18
|
+
#
|
14
19
|
class AuthenticityToken < Base
|
20
|
+
default_options :authenticity_param => 'authenticity_token'
|
21
|
+
|
15
22
|
def accepts?(env)
|
16
|
-
return true if safe? env
|
17
23
|
session = session env
|
18
24
|
token = session[:csrf] ||= session['_csrf_token'] || random_string
|
19
|
-
env
|
20
|
-
|
25
|
+
safe?(env) ||
|
26
|
+
secure_compare(env['HTTP_X_CSRF_TOKEN'].to_s, token) ||
|
27
|
+
secure_compare(Request.new(env).params[options[:authenticity_param]].to_s, token)
|
21
28
|
end
|
22
29
|
end
|
23
30
|
end
|
data/lib/rack/protection/base.rb
CHANGED
@@ -11,6 +11,7 @@ module Rack
|
|
11
11
|
:message => 'Forbidden', :encryptor => Digest::SHA1,
|
12
12
|
:session_key => 'rack.session', :status => 403,
|
13
13
|
:allow_empty_referrer => true,
|
14
|
+
:report_key => "protection.failed",
|
14
15
|
:html_types => %w[text/html application/xhtml]
|
15
16
|
}
|
16
17
|
|
@@ -42,7 +43,7 @@ module Rack
|
|
42
43
|
|
43
44
|
def call(env)
|
44
45
|
unless accepts? env
|
45
|
-
|
46
|
+
instrument env
|
46
47
|
result = react env
|
47
48
|
end
|
48
49
|
result or app.call(env)
|
@@ -59,10 +60,22 @@ module Rack
|
|
59
60
|
l.warn(message)
|
60
61
|
end
|
61
62
|
|
63
|
+
def instrument(env)
|
64
|
+
return unless i = options[:instrumenter]
|
65
|
+
env['rack.protection.attack'] = self.class.name.split('::').last.downcase
|
66
|
+
i.instrument('rack.protection', env)
|
67
|
+
end
|
68
|
+
|
62
69
|
def deny(env)
|
70
|
+
warn env, "attack prevented by #{self.class}"
|
63
71
|
[options[:status], {'Content-Type' => 'text/plain'}, [options[:message]]]
|
64
72
|
end
|
65
73
|
|
74
|
+
def report(env)
|
75
|
+
warn env, "attack reported by #{self.class}"
|
76
|
+
env[options[:report_key]] = true
|
77
|
+
end
|
78
|
+
|
66
79
|
def session?(env)
|
67
80
|
env.include? options[:session_key]
|
68
81
|
end
|
@@ -80,6 +93,7 @@ module Rack
|
|
80
93
|
ref = env['HTTP_REFERER'].to_s
|
81
94
|
return if !options[:allow_empty_referrer] and ref.empty?
|
82
95
|
URI.parse(ref).host || Request.new(env).host
|
96
|
+
rescue URI::InvalidURIError
|
83
97
|
end
|
84
98
|
|
85
99
|
def origin(env)
|
@@ -87,7 +101,7 @@ module Rack
|
|
87
101
|
end
|
88
102
|
|
89
103
|
def random_string(secure = defined? SecureRandom)
|
90
|
-
secure ? SecureRandom.hex(
|
104
|
+
secure ? SecureRandom.hex(16) : "%032x" % rand(2**128-1)
|
91
105
|
rescue NotImplementedError
|
92
106
|
random_string false
|
93
107
|
end
|
@@ -96,6 +110,30 @@ module Rack
|
|
96
110
|
options[:encryptor].hexdigest value.to_s
|
97
111
|
end
|
98
112
|
|
113
|
+
# The implementations of secure_compare and bytesize are taken from
|
114
|
+
# Rack::Utils to be able to support rack older than XXXX.
|
115
|
+
def secure_compare(a, b)
|
116
|
+
return false unless bytesize(a) == bytesize(b)
|
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
|
135
|
+
end
|
136
|
+
|
99
137
|
alias default_reaction deny
|
100
138
|
|
101
139
|
def html?(headers)
|
@@ -16,16 +16,22 @@ module Rack
|
|
16
16
|
# frame_options:: Defines who should be allowed to embed the page in a
|
17
17
|
# frame. Use :deny to forbid any embedding, :sameorigin
|
18
18
|
# to allow embedding from the same origin (default).
|
19
|
-
class FrameOptions <
|
19
|
+
class FrameOptions < Base
|
20
20
|
default_options :frame_options => :sameorigin
|
21
21
|
|
22
|
-
def
|
23
|
-
@
|
22
|
+
def frame_options
|
23
|
+
@frame_options ||= begin
|
24
24
|
frame_options = options[:frame_options]
|
25
25
|
frame_options = options[:frame_options].to_s.upcase unless frame_options.respond_to? :to_str
|
26
|
-
|
26
|
+
frame_options.to_str
|
27
27
|
end
|
28
28
|
end
|
29
|
+
|
30
|
+
def call(env)
|
31
|
+
status, headers, body = @app.call(env)
|
32
|
+
headers['X-Frame-Options'] ||= frame_options if html? headers
|
33
|
+
[status, headers, body]
|
34
|
+
end
|
29
35
|
end
|
30
36
|
end
|
31
37
|
end
|
@@ -11,17 +11,24 @@ module Rack
|
|
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
13
|
class JsonCsrf < Base
|
14
|
-
|
14
|
+
alias react deny
|
15
15
|
|
16
16
|
def call(env)
|
17
|
+
request = Request.new(env)
|
17
18
|
status, headers, body = app.call(env)
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
19
|
+
|
20
|
+
if has_vector? request, headers
|
21
|
+
warn env, "attack prevented by #{self.class}"
|
22
|
+
react(env) or [status, headers, body]
|
23
|
+
else
|
24
|
+
[status, headers, body]
|
23
25
|
end
|
24
|
-
|
26
|
+
end
|
27
|
+
|
28
|
+
def has_vector?(request, headers)
|
29
|
+
return false if request.xhr?
|
30
|
+
return false unless headers['Content-Type'].to_s.split(';', 2).first =~ /^\s*application\/json\s*$/
|
31
|
+
origin(request.env).nil? and referrer(request.env) != request.host
|
25
32
|
end
|
26
33
|
end
|
27
34
|
end
|
@@ -19,16 +19,30 @@ module Rack
|
|
19
19
|
end
|
20
20
|
|
21
21
|
def cleanup(path)
|
22
|
+
if path.respond_to?(:encoding)
|
23
|
+
# Ruby 1.9+ M17N
|
24
|
+
encoding = path.encoding
|
25
|
+
dot = '.'.encode(encoding)
|
26
|
+
slash = '/'.encode(encoding)
|
27
|
+
backslash = '\\'.encode(encoding)
|
28
|
+
else
|
29
|
+
# Ruby 1.8
|
30
|
+
dot = '.'
|
31
|
+
slash = '/'
|
32
|
+
backslash = '\\'
|
33
|
+
end
|
34
|
+
|
22
35
|
parts = []
|
23
|
-
unescaped = path.gsub(
|
36
|
+
unescaped = path.gsub(/%2e/i, dot).gsub(/%2f/i, slash).gsub(/%5c/i, backslash)
|
37
|
+
unescaped = unescaped.gsub(backslash, slash)
|
24
38
|
|
25
|
-
unescaped.split(
|
26
|
-
next if part.empty? or part ==
|
39
|
+
unescaped.split(slash).each do |part|
|
40
|
+
next if part.empty? or part == dot
|
27
41
|
part == '..' ? parts.pop : parts << part
|
28
42
|
end
|
29
43
|
|
30
|
-
cleaned =
|
31
|
-
cleaned <<
|
44
|
+
cleaned = slash + parts.join(slash)
|
45
|
+
cleaned << slash if parts.any? and unescaped =~ %r{/\.{0,2}$}
|
32
46
|
cleaned
|
33
47
|
end
|
34
48
|
end
|
@@ -9,9 +9,6 @@ module Rack
|
|
9
9
|
#
|
10
10
|
# Does not accept unsafe HTTP requests if the Referer [sic] header is set to
|
11
11
|
# a different host.
|
12
|
-
#
|
13
|
-
# Combine with NoReferrer to also block remote requests from non-HTTP pages
|
14
|
-
# (FTP/HTTPS/...).
|
15
12
|
class RemoteReferrer < Base
|
16
13
|
default_reaction :deny
|
17
14
|
|
@@ -9,12 +9,12 @@ module Rack
|
|
9
9
|
#
|
10
10
|
# Tracks request properties like the user agent in the session and empties
|
11
11
|
# the session if those properties change. This essentially prevents attacks
|
12
|
-
# from Firesheep. Since all headers taken into consideration
|
13
|
-
# spoofed, too, this will not prevent
|
12
|
+
# from Firesheep. Since all headers taken into consideration can be
|
13
|
+
# spoofed, too, this will not prevent determined hijacking attempts.
|
14
14
|
class SessionHijacking < Base
|
15
15
|
default_reaction :drop_session
|
16
16
|
default_options :tracking_key => :tracking, :encrypt_tracking => true,
|
17
|
-
:track => %w[HTTP_USER_AGENT
|
17
|
+
:track => %w[HTTP_USER_AGENT HTTP_ACCEPT_LANGUAGE]
|
18
18
|
|
19
19
|
def accepts?(env)
|
20
20
|
session = session env
|
@@ -14,18 +14,10 @@ module Rack
|
|
14
14
|
class XSSHeader < Base
|
15
15
|
default_options :xss_mode => :block, :nosniff => true
|
16
16
|
|
17
|
-
def header
|
18
|
-
headers = {
|
19
|
-
'X-XSS-Protection' => "1; mode=#{options[:xss_mode]}",
|
20
|
-
'X-Content-Type-Options' => "nosniff"
|
21
|
-
}
|
22
|
-
headers.delete("X-Content-Type-Options") unless options[:nosniff]
|
23
|
-
headers
|
24
|
-
end
|
25
|
-
|
26
17
|
def call(env)
|
27
18
|
status, headers, body = @app.call(env)
|
28
|
-
headers
|
19
|
+
headers['X-XSS-Protection'] ||= "1; mode=#{options[:xss_mode]}" if html? headers
|
20
|
+
headers['X-Content-Type-Options'] ||= 'nosniff' if options[:nosniff]
|
29
21
|
[status, headers, body]
|
30
22
|
end
|
31
23
|
end
|
data/rack-protection.gemspec
CHANGED
@@ -2,47 +2,74 @@
|
|
2
2
|
Gem::Specification.new do |s|
|
3
3
|
# general infos
|
4
4
|
s.name = "rack-protection"
|
5
|
-
s.version = "1.
|
5
|
+
s.version = "1.5.5"
|
6
6
|
s.description = "You should use protection!"
|
7
7
|
s.homepage = "http://github.com/rkh/rack-protection"
|
8
8
|
s.summary = s.description
|
9
|
+
s.license = 'MIT'
|
9
10
|
|
10
11
|
# generated from git shortlog -sn
|
11
12
|
s.authors = [
|
12
13
|
"Konstantin Haase",
|
13
14
|
"Alex Rodionov",
|
14
|
-
"
|
15
|
-
"
|
16
|
-
"
|
17
|
-
"
|
15
|
+
"Patrick Ellis",
|
16
|
+
"Jason Staten",
|
17
|
+
"ITO Nobuaki",
|
18
|
+
"Jeff Welling",
|
19
|
+
"Matteo Centenaro",
|
20
|
+
"Egor Homakov",
|
21
|
+
"Florian Gilcher",
|
18
22
|
"Fojas",
|
23
|
+
"Igor Bochkariov",
|
19
24
|
"Mael Clerambault",
|
20
25
|
"Martin Mauch",
|
26
|
+
"Renne Nissinen",
|
21
27
|
"SAKAI, Kazuaki",
|
22
28
|
"Stanislav Savulchik",
|
23
29
|
"Steve Agalloco",
|
24
|
-
"Akzhan Abdulin",
|
25
30
|
"TOBY",
|
26
|
-
"
|
31
|
+
"Thais Camilo and Konstantin Haase",
|
32
|
+
"Vipul A M",
|
33
|
+
"Akzhan Abdulin",
|
34
|
+
"brookemckim",
|
35
|
+
"Bj\u{f8}rge N\u{e6}ss",
|
36
|
+
"Chris Heald",
|
37
|
+
"Chris Mytton",
|
38
|
+
"Corey Ward",
|
39
|
+
"Dario Cravero",
|
40
|
+
"David Kellum"
|
27
41
|
]
|
28
42
|
|
29
43
|
# generated from git shortlog -sne
|
30
44
|
s.email = [
|
31
45
|
"konstantin.mailinglists@googlemail.com",
|
32
46
|
"p0deje@gmail.com",
|
33
|
-
"
|
34
|
-
"
|
35
|
-
"
|
36
|
-
"
|
47
|
+
"jstaten07@gmail.com",
|
48
|
+
"patrick@soundcloud.com",
|
49
|
+
"jeff.welling@gmail.com",
|
50
|
+
"bugant@gmail.com",
|
51
|
+
"daydream.trippers@gmail.com",
|
52
|
+
"florian.gilcher@asquera.de",
|
37
53
|
"developer@fojasaur.us",
|
54
|
+
"ujifgc@gmail.com",
|
38
55
|
"mael@clerambault.fr",
|
39
56
|
"martin.mauch@gmail.com",
|
57
|
+
"rennex@iki.fi",
|
40
58
|
"kaz.july.7@gmail.com",
|
41
59
|
"s.savulchik@gmail.com",
|
42
60
|
"steve.agalloco@gmail.com",
|
43
|
-
"akzhan.abdulin@gmail.com",
|
44
61
|
"toby.net.info.mail+git@gmail.com",
|
45
|
-
"
|
62
|
+
"dev+narwen+rkh@rkh.im",
|
63
|
+
"vipulnsward@gmail.com",
|
64
|
+
"akzhan.abdulin@gmail.com",
|
65
|
+
"brooke@digitalocean.com",
|
66
|
+
"bjoerge@bengler.no",
|
67
|
+
"cheald@gmail.com",
|
68
|
+
"self@hecticjeff.net",
|
69
|
+
"coreyward@me.com",
|
70
|
+
"dario@uxtemple.com",
|
71
|
+
"dek-oss@gravitext.com",
|
72
|
+
"homakov@gmail.com"
|
46
73
|
]
|
47
74
|
|
48
75
|
# generated from git ls-files
|
@@ -68,6 +95,7 @@ Gem::Specification.new do |s|
|
|
68
95
|
"lib/rack/protection/xss_header.rb",
|
69
96
|
"rack-protection.gemspec",
|
70
97
|
"spec/authenticity_token_spec.rb",
|
98
|
+
"spec/base_spec.rb",
|
71
99
|
"spec/escaped_params_spec.rb",
|
72
100
|
"spec/form_token_spec.rb",
|
73
101
|
"spec/frame_options_spec.rb",
|
@@ -30,4 +30,19 @@ describe Rack::Protection::AuthenticityToken do
|
|
30
30
|
it "prevents ajax requests without a valid token" do
|
31
31
|
post('/', {}, "HTTP_X_REQUESTED_WITH" => "XMLHttpRequest").should_not be_ok
|
32
32
|
end
|
33
|
+
|
34
|
+
it "allows for a custom authenticity token param" do
|
35
|
+
mock_app do
|
36
|
+
use Rack::Protection::AuthenticityToken, :authenticity_param => 'csrf_param'
|
37
|
+
run proc { |e| [200, {'Content-Type' => 'text/plain'}, ['hi']] }
|
38
|
+
end
|
39
|
+
|
40
|
+
post('/', {"csrf_param" => "a"}, 'rack.session' => {:csrf => "a"})
|
41
|
+
last_response.should be_ok
|
42
|
+
end
|
43
|
+
|
44
|
+
it "sets a new csrf token for the session in env, even after a 'safe' request" do
|
45
|
+
get('/', {}, {})
|
46
|
+
env['rack.session'][:csrf].should_not be_nil
|
47
|
+
end
|
33
48
|
end
|
data/spec/base_spec.rb
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
require File.expand_path('../spec_helper.rb', __FILE__)
|
2
|
+
|
3
|
+
describe Rack::Protection::Base do
|
4
|
+
|
5
|
+
subject { described_class.new(lambda {}) }
|
6
|
+
|
7
|
+
describe "#random_string" do
|
8
|
+
it "outputs a string of 32 characters" do
|
9
|
+
subject.random_string.length.should == 32
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
describe "#referrer" do
|
14
|
+
it "Reads referrer from Referer header" do
|
15
|
+
env = {"HTTP_HOST" => "foo.com", "HTTP_REFERER" => "http://bar.com/valid"}
|
16
|
+
subject.referrer(env).should == "bar.com"
|
17
|
+
end
|
18
|
+
|
19
|
+
it "Reads referrer from Host header when Referer header is relative" do
|
20
|
+
env = {"HTTP_HOST" => "foo.com", "HTTP_REFERER" => "/valid"}
|
21
|
+
subject.referrer(env).should == "foo.com"
|
22
|
+
end
|
23
|
+
|
24
|
+
it "Reads referrer from Host header when Referer header is missing" do
|
25
|
+
env = {"HTTP_HOST" => "foo.com"}
|
26
|
+
subject.referrer(env).should == "foo.com"
|
27
|
+
end
|
28
|
+
|
29
|
+
it "Returns nil when Referer header is missing and allow_empty_referrer is false" do
|
30
|
+
env = {"HTTP_HOST" => "foo.com"}
|
31
|
+
subject.options[:allow_empty_referrer] = false
|
32
|
+
subject.referrer(env).should be_nil
|
33
|
+
end
|
34
|
+
|
35
|
+
it "Returns nil when Referer header is invalid" do
|
36
|
+
env = {"HTTP_HOST" => "foo.com", "HTTP_REFERER" => "http://bar.com/bad|uri"}
|
37
|
+
subject.referrer(env).should be_nil
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
data/spec/escaped_params_spec.rb
CHANGED
data/spec/json_csrf_spec.rb
CHANGED
@@ -27,6 +27,11 @@ describe Rack::Protection::JsonCsrf do
|
|
27
27
|
it "accepts get requests with json responses with no referrer" do
|
28
28
|
get('/', {}).should be_ok
|
29
29
|
end
|
30
|
+
|
31
|
+
it "accepts XHR requests" do
|
32
|
+
get('/', {}, 'HTTP_REFERER' => 'http://evil.com', 'HTTP_X_REQUESTED_WITH' => 'XMLHttpRequest').should be_ok
|
33
|
+
end
|
34
|
+
|
30
35
|
end
|
31
36
|
|
32
37
|
describe 'not json response' do
|
@@ -37,4 +42,17 @@ describe Rack::Protection::JsonCsrf do
|
|
37
42
|
end
|
38
43
|
|
39
44
|
end
|
45
|
+
|
46
|
+
describe 'with drop_session as default reaction' do
|
47
|
+
it 'still denies' do
|
48
|
+
mock_app do
|
49
|
+
use Rack::Protection, :reaction => :drop_session
|
50
|
+
run proc { |e| [200, {'Content-Type' => 'application/json'}, []]}
|
51
|
+
end
|
52
|
+
|
53
|
+
session = {:foo => :bar}
|
54
|
+
get('/', {}, 'HTTP_REFERER' => 'http://evil.com', 'rack.session' => session)
|
55
|
+
last_response.should_not be_ok
|
56
|
+
end
|
57
|
+
end
|
40
58
|
end
|
data/spec/path_traversal_spec.rb
CHANGED
@@ -14,8 +14,8 @@ describe Rack::Protection::PathTraversal do
|
|
14
14
|
|
15
15
|
{ # yes, this is ugly, feel free to change that
|
16
16
|
'/..' => '/', '/a/../b' => '/b', '/a/../b/' => '/b/', '/a/.' => '/a/',
|
17
|
-
'/%2e.' => '/', '/a/%
|
18
|
-
'//' => '/', '/%2fetc%
|
17
|
+
'/%2e.' => '/', '/a/%2E%2e/b' => '/b', '/a%2f%2E%2e%2Fb/' => '/b/',
|
18
|
+
'//' => '/', '/%2fetc%2Fpasswd' => '/etc/passwd'
|
19
19
|
}.each do |a, b|
|
20
20
|
it("replaces #{a.inspect} with #{b.inspect}") { get(a).body.should == b }
|
21
21
|
end
|
@@ -25,4 +25,17 @@ describe Rack::Protection::PathTraversal do
|
|
25
25
|
app.call({}).should be == 42
|
26
26
|
end
|
27
27
|
end
|
28
|
+
|
29
|
+
if "".respond_to?(:encoding) # Ruby 1.9+ M17N
|
30
|
+
context "PATH_INFO's encoding" do
|
31
|
+
before do
|
32
|
+
@app = Rack::Protection::PathTraversal.new(proc { |e| [200, {'Content-Type' => 'text/plain'}, [e['PATH_INFO'].encoding.to_s]] })
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'should remain unchanged as ASCII-8BIT' do
|
36
|
+
body = @app.call({ 'PATH_INFO' => '/'.encode('ASCII-8BIT') })[2][0]
|
37
|
+
body.should == 'ASCII-8BIT'
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
28
41
|
end
|
data/spec/protection_spec.rb
CHANGED
@@ -18,6 +18,53 @@ describe Rack::Protection do
|
|
18
18
|
session.should be_empty
|
19
19
|
end
|
20
20
|
|
21
|
+
it 'passes errors through if :reaction => :report is used' do
|
22
|
+
mock_app do
|
23
|
+
use Rack::Protection, :reaction => :report
|
24
|
+
run proc { |e| [200, {'Content-Type' => 'text/plain'}, [e["protection.failed"].to_s]] }
|
25
|
+
end
|
26
|
+
|
27
|
+
session = {:foo => :bar}
|
28
|
+
post('/', {}, 'rack.session' => session, 'HTTP_ORIGIN' => 'http://malicious.com')
|
29
|
+
last_response.should be_ok
|
30
|
+
body.should == "true"
|
31
|
+
end
|
32
|
+
|
33
|
+
describe "#react" do
|
34
|
+
it 'prevents attacks and warns about it' do
|
35
|
+
io = StringIO.new
|
36
|
+
mock_app do
|
37
|
+
use Rack::Protection, :logger => Logger.new(io)
|
38
|
+
run DummyApp
|
39
|
+
end
|
40
|
+
post('/', {}, 'rack.session' => {}, 'HTTP_ORIGIN' => 'http://malicious.com')
|
41
|
+
io.string.should match /prevented.*Origin/
|
42
|
+
end
|
43
|
+
|
44
|
+
it 'reports attacks if reaction is to report' do
|
45
|
+
io = StringIO.new
|
46
|
+
mock_app do
|
47
|
+
use Rack::Protection, :reaction => :report, :logger => Logger.new(io)
|
48
|
+
run DummyApp
|
49
|
+
end
|
50
|
+
post('/', {}, 'rack.session' => {}, 'HTTP_ORIGIN' => 'http://malicious.com')
|
51
|
+
io.string.should match /reported.*Origin/
|
52
|
+
io.string.should_not match /prevented.*Origin/
|
53
|
+
end
|
54
|
+
|
55
|
+
it 'passes errors to reaction method if specified' do
|
56
|
+
io = StringIO.new
|
57
|
+
Rack::Protection::Base.send(:define_method, :special) { |*args| io << args.inspect }
|
58
|
+
mock_app do
|
59
|
+
use Rack::Protection, :reaction => :special, :logger => Logger.new(io)
|
60
|
+
run DummyApp
|
61
|
+
end
|
62
|
+
post('/', {}, 'rack.session' => {}, 'HTTP_ORIGIN' => 'http://malicious.com')
|
63
|
+
io.string.should match /HTTP_ORIGIN.*malicious.com/
|
64
|
+
io.string.should_not match /reported|prevented/
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
21
68
|
describe "#html?" do
|
22
69
|
context "given an appropriate content-type header" do
|
23
70
|
subject { Rack::Protection::Base.new(nil).html? 'content-type' => "text/html" }
|
@@ -34,4 +81,25 @@ describe Rack::Protection do
|
|
34
81
|
it { should be_false }
|
35
82
|
end
|
36
83
|
end
|
84
|
+
|
85
|
+
describe "#instrument" do
|
86
|
+
let(:env) { { 'rack.protection.attack' => 'base' } }
|
87
|
+
let(:instrumenter) { double('Instrumenter') }
|
88
|
+
|
89
|
+
after do
|
90
|
+
app.instrument(env)
|
91
|
+
end
|
92
|
+
|
93
|
+
context 'with an instrumenter specified' do
|
94
|
+
let(:app) { Rack::Protection::Base.new(nil, :instrumenter => instrumenter) }
|
95
|
+
|
96
|
+
it { instrumenter.should_receive(:instrument).with('rack.protection', env) }
|
97
|
+
end
|
98
|
+
|
99
|
+
context 'with no instrumenter specified' do
|
100
|
+
let(:app) { Rack::Protection::Base.new(nil) }
|
101
|
+
|
102
|
+
it { instrumenter.should_not_receive(:instrument) }
|
103
|
+
end
|
104
|
+
end
|
37
105
|
end
|
@@ -17,11 +17,12 @@ describe Rack::Protection::SessionHijacking do
|
|
17
17
|
session.should be_empty
|
18
18
|
end
|
19
19
|
|
20
|
-
it "
|
20
|
+
it "accepts requests with a changing Accept-Encoding header" do
|
21
|
+
# this is tested because previously it led to clearing the session
|
21
22
|
session = {:foo => :bar}
|
22
23
|
get '/', {}, 'rack.session' => session, 'HTTP_ACCEPT_ENCODING' => 'a'
|
23
24
|
get '/', {}, 'rack.session' => session, 'HTTP_ACCEPT_ENCODING' => 'b'
|
24
|
-
session.
|
25
|
+
session.should_not be_empty
|
25
26
|
end
|
26
27
|
|
27
28
|
it "denies requests with a changing Accept-Language header" do
|
data/spec/xss_header_spec.rb
CHANGED
@@ -34,6 +34,12 @@ describe Rack::Protection::XSSHeader do
|
|
34
34
|
get('/', {}, 'wants' => 'text/html').header["X-Content-Type-Options"].should == "nosniff"
|
35
35
|
end
|
36
36
|
|
37
|
+
|
38
|
+
it 'should set the X-Content-Type-Options for other content types' do
|
39
|
+
get('/', {}, 'wants' => 'application/foo').header["X-Content-Type-Options"].should == "nosniff"
|
40
|
+
end
|
41
|
+
|
42
|
+
|
37
43
|
it 'should allow changing the nosniff-mode off' do
|
38
44
|
mock_app do
|
39
45
|
use Rack::Protection::XSSHeader, :nosniff => false
|
metadata
CHANGED
@@ -1,95 +1,114 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rack-protection
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
5
|
-
prerelease:
|
4
|
+
version: 1.5.5
|
6
5
|
platform: ruby
|
7
6
|
authors:
|
8
7
|
- Konstantin Haase
|
9
8
|
- Alex Rodionov
|
10
|
-
-
|
11
|
-
-
|
12
|
-
-
|
13
|
-
-
|
9
|
+
- Patrick Ellis
|
10
|
+
- Jason Staten
|
11
|
+
- ITO Nobuaki
|
12
|
+
- Jeff Welling
|
13
|
+
- Matteo Centenaro
|
14
|
+
- Egor Homakov
|
15
|
+
- Florian Gilcher
|
14
16
|
- Fojas
|
17
|
+
- Igor Bochkariov
|
15
18
|
- Mael Clerambault
|
16
19
|
- Martin Mauch
|
20
|
+
- Renne Nissinen
|
17
21
|
- SAKAI, Kazuaki
|
18
22
|
- Stanislav Savulchik
|
19
23
|
- Steve Agalloco
|
20
|
-
- Akzhan Abdulin
|
21
24
|
- TOBY
|
25
|
+
- Thais Camilo and Konstantin Haase
|
26
|
+
- Vipul A M
|
27
|
+
- Akzhan Abdulin
|
28
|
+
- brookemckim
|
22
29
|
- Bjørge Næss
|
30
|
+
- Chris Heald
|
31
|
+
- Chris Mytton
|
32
|
+
- Corey Ward
|
33
|
+
- Dario Cravero
|
34
|
+
- David Kellum
|
23
35
|
autorequire:
|
24
36
|
bindir: bin
|
25
37
|
cert_chain: []
|
26
|
-
date:
|
38
|
+
date: 2018-03-07 00:00:00.000000000 Z
|
27
39
|
dependencies:
|
28
40
|
- !ruby/object:Gem::Dependency
|
29
41
|
name: rack
|
30
42
|
requirement: !ruby/object:Gem::Requirement
|
31
|
-
none: false
|
32
43
|
requirements:
|
33
|
-
- -
|
44
|
+
- - ">="
|
34
45
|
- !ruby/object:Gem::Version
|
35
46
|
version: '0'
|
36
47
|
type: :runtime
|
37
48
|
prerelease: false
|
38
49
|
version_requirements: !ruby/object:Gem::Requirement
|
39
|
-
none: false
|
40
50
|
requirements:
|
41
|
-
- -
|
51
|
+
- - ">="
|
42
52
|
- !ruby/object:Gem::Version
|
43
53
|
version: '0'
|
44
54
|
- !ruby/object:Gem::Dependency
|
45
55
|
name: rack-test
|
46
56
|
requirement: !ruby/object:Gem::Requirement
|
47
|
-
none: false
|
48
57
|
requirements:
|
49
|
-
- -
|
58
|
+
- - ">="
|
50
59
|
- !ruby/object:Gem::Version
|
51
60
|
version: '0'
|
52
61
|
type: :development
|
53
62
|
prerelease: false
|
54
63
|
version_requirements: !ruby/object:Gem::Requirement
|
55
|
-
none: false
|
56
64
|
requirements:
|
57
|
-
- -
|
65
|
+
- - ">="
|
58
66
|
- !ruby/object:Gem::Version
|
59
67
|
version: '0'
|
60
68
|
- !ruby/object:Gem::Dependency
|
61
69
|
name: rspec
|
62
70
|
requirement: !ruby/object:Gem::Requirement
|
63
|
-
none: false
|
64
71
|
requirements:
|
65
|
-
- - ~>
|
72
|
+
- - "~>"
|
66
73
|
- !ruby/object:Gem::Version
|
67
74
|
version: '2.0'
|
68
75
|
type: :development
|
69
76
|
prerelease: false
|
70
77
|
version_requirements: !ruby/object:Gem::Requirement
|
71
|
-
none: false
|
72
78
|
requirements:
|
73
|
-
- - ~>
|
79
|
+
- - "~>"
|
74
80
|
- !ruby/object:Gem::Version
|
75
81
|
version: '2.0'
|
76
82
|
description: You should use protection!
|
77
83
|
email:
|
78
84
|
- konstantin.mailinglists@googlemail.com
|
79
85
|
- p0deje@gmail.com
|
80
|
-
-
|
81
|
-
-
|
82
|
-
-
|
83
|
-
-
|
86
|
+
- jstaten07@gmail.com
|
87
|
+
- patrick@soundcloud.com
|
88
|
+
- jeff.welling@gmail.com
|
89
|
+
- bugant@gmail.com
|
90
|
+
- daydream.trippers@gmail.com
|
91
|
+
- florian.gilcher@asquera.de
|
84
92
|
- developer@fojasaur.us
|
93
|
+
- ujifgc@gmail.com
|
85
94
|
- mael@clerambault.fr
|
86
95
|
- martin.mauch@gmail.com
|
96
|
+
- rennex@iki.fi
|
87
97
|
- kaz.july.7@gmail.com
|
88
98
|
- s.savulchik@gmail.com
|
89
99
|
- steve.agalloco@gmail.com
|
90
|
-
- akzhan.abdulin@gmail.com
|
91
100
|
- toby.net.info.mail+git@gmail.com
|
101
|
+
- dev+narwen+rkh@rkh.im
|
102
|
+
- vipulnsward@gmail.com
|
103
|
+
- akzhan.abdulin@gmail.com
|
104
|
+
- brooke@digitalocean.com
|
92
105
|
- bjoerge@bengler.no
|
106
|
+
- cheald@gmail.com
|
107
|
+
- self@hecticjeff.net
|
108
|
+
- coreyward@me.com
|
109
|
+
- dario@uxtemple.com
|
110
|
+
- dek-oss@gravitext.com
|
111
|
+
- homakov@gmail.com
|
93
112
|
executables: []
|
94
113
|
extensions: []
|
95
114
|
extra_rdoc_files: []
|
@@ -115,6 +134,7 @@ files:
|
|
115
134
|
- lib/rack/protection/xss_header.rb
|
116
135
|
- rack-protection.gemspec
|
117
136
|
- spec/authenticity_token_spec.rb
|
137
|
+
- spec/base_spec.rb
|
118
138
|
- spec/escaped_params_spec.rb
|
119
139
|
- spec/form_token_spec.rb
|
120
140
|
- spec/frame_options_spec.rb
|
@@ -129,28 +149,27 @@ files:
|
|
129
149
|
- spec/spec_helper.rb
|
130
150
|
- spec/xss_header_spec.rb
|
131
151
|
homepage: http://github.com/rkh/rack-protection
|
132
|
-
licenses:
|
152
|
+
licenses:
|
153
|
+
- MIT
|
154
|
+
metadata: {}
|
133
155
|
post_install_message:
|
134
156
|
rdoc_options: []
|
135
157
|
require_paths:
|
136
158
|
- lib
|
137
159
|
required_ruby_version: !ruby/object:Gem::Requirement
|
138
|
-
none: false
|
139
160
|
requirements:
|
140
|
-
- -
|
161
|
+
- - ">="
|
141
162
|
- !ruby/object:Gem::Version
|
142
163
|
version: '0'
|
143
164
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
144
|
-
none: false
|
145
165
|
requirements:
|
146
|
-
- -
|
166
|
+
- - ">="
|
147
167
|
- !ruby/object:Gem::Version
|
148
168
|
version: '0'
|
149
169
|
requirements: []
|
150
170
|
rubyforge_project:
|
151
|
-
rubygems_version:
|
171
|
+
rubygems_version: 2.7.3
|
152
172
|
signing_key:
|
153
|
-
specification_version:
|
173
|
+
specification_version: 4
|
154
174
|
summary: You should use protection!
|
155
175
|
test_files: []
|
156
|
-
has_rdoc:
|