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 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.