rack-protection 1.5.5 → 2.0.0.beta1
Sign up to get free protection for your applications and to get access to all the features.
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
|
+
[![Build Status](https://secure.travis-ci.org/sinatra/rack-protection.png)](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.
|