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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA256:
3
- metadata.gz: 2b7d78da301d9f7fc81ae73e46a389c2b8ce10ab8121f169fd760018ac506d47
4
- data.tar.gz: a91bd28f8624f325d6714262ac11d83e0a347405f14e600780cb1bfd846e5b34
2
+ SHA1:
3
+ metadata.gz: 64d5507a2ecfe3df38d6e733ba3b27ad3c9f9c4a
4
+ data.tar.gz: dd394521e2416add01c08698ecb63921a14f97b1
5
5
  SHA512:
6
- metadata.gz: 0c5de92c0283313c00d50c1f9a219c808ad587caabff81c4d1530abd8f0e7d9c0f3753ad9bab7c06a29ce97b2a717fddc04ced642adff058f3431419286e4da6
7
- data.tar.gz: d3bf5830bf30475871b73ba54ee38f962bd93c2e1f420b59b649d6dcb7f97d89f091d3add3f692ba847ffe5f5c6cade665d522c86220087d977fb3706e41bd58
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
- You should use protection!
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 => `git shortlog -sne`.force_encoding('utf-8').scan(/[^<]+@[^>]+/),
23
- :files => `git ls-files`.force_encoding('utf-8').split("\n").reject { |f| f =~ /^(\.|Gemfile)/ }
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} = ["
@@ -3,36 +3,50 @@ require 'rack'
3
3
 
4
4
  module Rack
5
5
  module Protection
6
- autoload :AuthenticityToken, 'rack/protection/authenticity_token'
7
- autoload :Base, 'rack/protection/base'
8
- autoload :EscapedParams, 'rack/protection/escaped_params'
9
- autoload :FormToken, 'rack/protection/form_token'
10
- autoload :FrameOptions, 'rack/protection/frame_options'
11
- autoload :HttpOrigin, 'rack/protection/http_origin'
12
- autoload :IPSpoofing, 'rack/protection/ip_spoofing'
13
- autoload :JsonCsrf, 'rack/protection/json_csrf'
14
- autoload :PathTraversal, 'rack/protection/path_traversal'
15
- autoload :RemoteReferrer, 'rack/protection/remote_referrer'
16
- autoload :RemoteToken, 'rack/protection/remote_token'
17
- autoload :SessionHijacking, 'rack/protection/session_hijacking'
18
- autoload :XSSHeader, 'rack/protection/xss_header'
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
- 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
28
- use ::Rack::Protection::FrameOptions, options unless except.include? :frame_options
29
- use ::Rack::Protection::HttpOrigin, options unless except.include? :http_origin
30
- use ::Rack::Protection::IPSpoofing, options unless except.include? :ip_spoofing
31
- use ::Rack::Protection::JsonCsrf, options unless except.include? :json_csrf
32
- use ::Rack::Protection::PathTraversal, options unless except.include? :path_traversal
33
- use ::Rack::Protection::RemoteToken, options unless except.include? :remote_token
34
- use ::Rack::Protection::SessionHijacking, options unless except.include? :session_hijacking
35
- use ::Rack::Protection::XSSHeader, options unless except.include? :xss_header
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
- token = session[:csrf] ||= session['_csrf_token'] || random_string
73
+ session[:csrf] ||= self.class.random_token(token_length)
74
+
25
75
  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)
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
@@ -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
- 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
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? request, headers
23
+ if has_vector?(request, headers)
21
24
  warn env, "attack prevented by #{self.class}"
22
- react(env) or [status, headers, body]
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).gsub(/%5c/i, backslash)
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
- def self.version
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 later
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.