rack-protection 1.5.5 → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +13 -0
  3. data/License +4 -1
  4. data/README.md +41 -13
  5. data/Rakefile +29 -5
  6. data/lib/rack/protection.rb +41 -24
  7. data/lib/rack/protection/authenticity_token.rb +181 -9
  8. data/lib/rack/protection/base.rb +3 -22
  9. data/lib/rack/protection/content_security_policy.rb +79 -0
  10. data/lib/rack/protection/cookie_tossing.rb +75 -0
  11. data/lib/rack/protection/escaped_params.rb +2 -0
  12. data/lib/rack/protection/form_token.rb +1 -1
  13. data/lib/rack/protection/http_origin.rb +17 -2
  14. data/lib/rack/protection/json_csrf.rb +26 -4
  15. data/lib/rack/protection/path_traversal.rb +4 -12
  16. data/lib/rack/protection/referrer_policy.rb +25 -0
  17. data/lib/rack/protection/remote_token.rb +1 -1
  18. data/lib/rack/protection/session_hijacking.rb +1 -1
  19. data/lib/rack/protection/strict_transport.rb +39 -0
  20. data/lib/rack/protection/version.rb +1 -12
  21. data/lib/rack/protection/xss_header.rb +1 -1
  22. data/rack-protection.gemspec +26 -104
  23. metadata +21 -82
  24. data/spec/authenticity_token_spec.rb +0 -48
  25. data/spec/base_spec.rb +0 -40
  26. data/spec/escaped_params_spec.rb +0 -43
  27. data/spec/form_token_spec.rb +0 -33
  28. data/spec/frame_options_spec.rb +0 -39
  29. data/spec/http_origin_spec.rb +0 -38
  30. data/spec/ip_spoofing_spec.rb +0 -35
  31. data/spec/json_csrf_spec.rb +0 -58
  32. data/spec/path_traversal_spec.rb +0 -41
  33. data/spec/protection_spec.rb +0 -105
  34. data/spec/remote_referrer_spec.rb +0 -31
  35. data/spec/remote_token_spec.rb +0 -42
  36. data/spec/session_hijacking_spec.rb +0 -55
  37. data/spec/spec_helper.rb +0 -163
  38. data/spec/xss_header_spec.rb +0 -56
@@ -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'
@@ -12,7 +13,7 @@ module Rack
12
13
  :session_key => 'rack.session', :status => 403,
13
14
  :allow_empty_referrer => true,
14
15
  :report_key => "protection.failed",
15
- :html_types => %w[text/html application/xhtml]
16
+ :html_types => %w[text/html application/xhtml text/xml application/xml]
16
17
  }
17
18
 
18
19
  attr_reader :app, :options
@@ -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
@@ -0,0 +1,79 @@
1
+ # -*- coding: utf-8 -*-
2
+ require 'rack/protection'
3
+
4
+ module Rack
5
+ module Protection
6
+ ##
7
+ # Prevented attack:: XSS and others
8
+ # Supported browsers:: Firefox 23+, Safari 7+, Chrome 25+, Opera 15+
9
+ #
10
+ # Description:: Content Security Policy, a mechanism web applications
11
+ # can use to mitigate a broad class of content injection
12
+ # vulnerabilities, such as cross-site scripting (XSS).
13
+ # Content Security Policy is a declarative policy that lets
14
+ # the authors (or server administrators) of a web application
15
+ # inform the client about the sources from which the
16
+ # application expects to load resources.
17
+ #
18
+ # More info:: W3C CSP Level 1 : https://www.w3.org/TR/CSP1/ (deprecated)
19
+ # W3C CSP Level 2 : https://www.w3.org/TR/CSP2/ (current)
20
+ # W3C CSP Level 3 : https://www.w3.org/TR/CSP3/ (draft)
21
+ # https://developer.mozilla.org/en-US/docs/Web/Security/CSP
22
+ # http://caniuse.com/#search=ContentSecurityPolicy
23
+ # http://content-security-policy.com/
24
+ # https://securityheaders.io
25
+ # https://scotthelme.co.uk/csp-cheat-sheet/
26
+ # http://www.html5rocks.com/en/tutorials/security/content-security-policy/
27
+ #
28
+ # Sets the 'Content-Security-Policy[-Report-Only]' header.
29
+ #
30
+ # Options: ContentSecurityPolicy configuration is a complex topic with
31
+ # several levels of support that has evolved over time.
32
+ # See the W3C documentation and the links in the more info
33
+ # section for CSP usage examples and best practices. The
34
+ # CSP3 directives in the 'NO_ARG_DIRECTIVES' constant need to be
35
+ # presented in the options hash with a boolean 'true' in order
36
+ # to be used in a policy.
37
+ #
38
+ class ContentSecurityPolicy < Base
39
+ default_options default_src: "'self'", report_only: false
40
+
41
+ DIRECTIVES = %i(base_uri child_src connect_src default_src
42
+ font_src form_action frame_ancestors frame_src
43
+ img_src manifest_src media_src object_src
44
+ plugin_types referrer reflected_xss report_to
45
+ report_uri require_sri_for sandbox script_src
46
+ style_src worker_src webrtc_src navigate_to
47
+ prefetch_src).freeze
48
+
49
+ NO_ARG_DIRECTIVES = %i(block_all_mixed_content disown_opener
50
+ upgrade_insecure_requests).freeze
51
+
52
+ def csp_policy
53
+ directives = []
54
+
55
+ DIRECTIVES.each do |d|
56
+ if options.key?(d)
57
+ directives << "#{d.to_s.sub(/_/, '-')} #{options[d]}"
58
+ end
59
+ end
60
+
61
+ # Set these key values to boolean 'true' to include in policy
62
+ NO_ARG_DIRECTIVES.each do |d|
63
+ if options.key?(d) && options[d].is_a?(TrueClass)
64
+ directives << d.to_s.tr('_', '-')
65
+ end
66
+ end
67
+
68
+ directives.compact.sort.join('; ')
69
+ end
70
+
71
+ def call(env)
72
+ status, headers, body = @app.call(env)
73
+ header = options[:report_only] ? 'Content-Security-Policy-Report-Only' : 'Content-Security-Policy'
74
+ headers[header] ||= csp_policy if html? headers
75
+ [status, headers, body]
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,75 @@
1
+ require 'rack/protection'
2
+ require 'pathname'
3
+
4
+ module Rack
5
+ module Protection
6
+ ##
7
+ # Prevented attack:: Cookie Tossing
8
+ # Supported browsers:: all
9
+ # More infos:: https://github.com/blog/1466-yummy-cookies-across-domains
10
+ #
11
+ # Does not accept HTTP requests if the HTTP_COOKIE header contains more than one
12
+ # session cookie. This does not protect against a cookie overflow attack.
13
+ #
14
+ # Options:
15
+ #
16
+ # session_key:: The name of the session cookie (default: 'rack.session')
17
+ class CookieTossing < Base
18
+ default_reaction :deny
19
+
20
+ def call(env)
21
+ status, headers, body = super
22
+ response = Rack::Response.new(body, status, headers)
23
+ request = Rack::Request.new(env)
24
+ remove_bad_cookies(request, response)
25
+ response.finish
26
+ end
27
+
28
+ def accepts?(env)
29
+ cookie_header = env['HTTP_COOKIE']
30
+ cookies = Rack::Utils.parse_query(cookie_header, ';,') { |s| s }
31
+ cookies.each do |k, v|
32
+ if k == session_key && Array(v).size > 1
33
+ bad_cookies << k
34
+ elsif k != session_key && Rack::Utils.unescape(k) == session_key
35
+ bad_cookies << k
36
+ end
37
+ end
38
+ bad_cookies.empty?
39
+ end
40
+
41
+ def remove_bad_cookies(request, response)
42
+ return if bad_cookies.empty?
43
+ paths = cookie_paths(request.path)
44
+ bad_cookies.each do |name|
45
+ paths.each { |path| response.set_cookie name, empty_cookie(request.host, path) }
46
+ end
47
+ end
48
+
49
+ def redirect(env)
50
+ request = Request.new(env)
51
+ warn env, "attack prevented by #{self.class}"
52
+ [302, {'Content-Type' => 'text/html', 'Location' => request.path}, []]
53
+ end
54
+
55
+ def bad_cookies
56
+ @bad_cookies ||= []
57
+ end
58
+
59
+ def cookie_paths(path)
60
+ path = '/' if path.to_s.empty?
61
+ paths = []
62
+ Pathname.new(path).descend { |p| paths << p.to_s }
63
+ paths
64
+ end
65
+
66
+ def empty_cookie(host, path)
67
+ {:value => '', :domain => host, :path => path, :expires => Time.at(0)}
68
+ end
69
+
70
+ def session_key
71
+ @session_key ||= options[:session_key]
72
+ end
73
+ end
74
+ end
75
+ end
@@ -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
@@ -13,7 +13,7 @@ module Rack
13
13
  # This middleware is not used when using the Rack::Protection collection,
14
14
  # since it might be a security issue, depending on your application
15
15
  #
16
- # Compatible with Rails and rack-csrf.
16
+ # Compatible with rack-csrf.
17
17
  class FormToken < AuthenticityToken
18
18
  def accepts?(env)
19
19
  env["HTTP_X_REQUESTED_WITH"] == "XMLHttpRequest" or super
@@ -9,10 +9,17 @@ module Rack
9
9
  # http://tools.ietf.org/html/draft-abarth-origin
10
10
  #
11
11
  # Does not accept unsafe HTTP requests when value of Origin HTTP request header
12
- # does not match default or whitelisted URIs.
12
+ # does not match default or permitted URIs.
13
+ #
14
+ # If you want to permit a specific domain, you can pass in as the `:permitted_origins` option:
15
+ #
16
+ # use Rack::Protection, permitted_origins: ["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,7 +31,15 @@ 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
27
- Array(options[:origin_whitelist]).include? origin
34
+ return true if options[:allow_if] && options[:allow_if].call(env)
35
+
36
+ if options.key? :origin_whitelist
37
+ warn "Rack::Protection origin_whitelist option is deprecated and will be removed, " \
38
+ "use permitted_origins instead.\n"
39
+ end
40
+
41
+ permitted_origins = options[:permitted_origins] || options[:origin_whitelist]
42
+ Array(permitted_origins).include? origin
28
43
  end
29
44
 
30
45
  end
@@ -5,21 +5,30 @@ module Rack
5
5
  ##
6
6
  # Prevented attack:: CSRF
7
7
  # Supported browsers:: all
8
- # More infos:: http://flask.pocoo.org/docs/security/#json-security
8
+ # More infos:: http://flask.pocoo.org/docs/0.10/security/#json-security
9
+ # http://haacked.com/archive/2008/11/20/anatomy-of-a-subtle-json-vulnerability.aspx
9
10
  #
10
- # JSON GET APIs are vulnerable to being embedded as JavaScript while the
11
+ # JSON GET APIs are vulnerable to being embedded as JavaScript when the
11
12
  # Array prototype has been patched to track data. Checks the referrer
12
13
  # even on GET requests if the content type is JSON.
14
+ #
15
+ # If request includes Origin HTTP header, defers to HttpOrigin to determine
16
+ # if the request is safe. Please refer to the documentation for more info.
17
+ #
18
+ # The `:allow_if` option can be set to a proc to use custom allow/deny logic.
13
19
  class JsonCsrf < Base
20
+ default_options :allow_if => nil
21
+
14
22
  alias react deny
15
23
 
16
24
  def call(env)
17
25
  request = Request.new(env)
18
26
  status, headers, body = app.call(env)
19
27
 
20
- if has_vector? request, headers
28
+ if has_vector?(request, headers)
21
29
  warn env, "attack prevented by #{self.class}"
22
- react(env) or [status, headers, body]
30
+
31
+ react_and_close(env, body) or [status, headers, body]
23
32
  else
24
33
  [status, headers, body]
25
34
  end
@@ -27,9 +36,22 @@ module Rack
27
36
 
28
37
  def has_vector?(request, headers)
29
38
  return false if request.xhr?
39
+ return false if options[:allow_if] && options[:allow_if].call(request.env)
30
40
  return false unless headers['Content-Type'].to_s.split(';', 2).first =~ /^\s*application\/json\s*$/
31
41
  origin(request.env).nil? and referrer(request.env) != request.host
32
42
  end
43
+
44
+ def react_and_close(env, body)
45
+ reaction = react(env)
46
+
47
+ close_body(body) if reaction
48
+
49
+ reaction
50
+ end
51
+
52
+ def close_body(body)
53
+ body.close if body.respond_to?(:close)
54
+ end
33
55
  end
34
56
  end
35
57
  end
@@ -19,18 +19,10 @@ 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
22
+ encoding = path.encoding
23
+ dot = '.'.encode(encoding)
24
+ slash = '/'.encode(encoding)
25
+ backslash = '\\'.encode(encoding)
34
26
 
35
27
  parts = []
36
28
  unescaped = path.gsub(/%2e/i, dot).gsub(/%2f/i, slash).gsub(/%5c/i, backslash)
@@ -0,0 +1,25 @@
1
+ require 'rack/protection'
2
+
3
+ module Rack
4
+ module Protection
5
+ ##
6
+ # Prevented attack:: Secret leakage, third party tracking
7
+ # Supported browsers:: mixed support
8
+ # More infos:: https://www.w3.org/TR/referrer-policy/
9
+ # https://caniuse.com/#search=referrer-policy
10
+ #
11
+ # Sets Referrer-Policy header to tell the browser to limit the Referer header.
12
+ #
13
+ # Options:
14
+ # referrer_policy:: The policy to use (default: 'strict-origin-when-cross-origin')
15
+ class ReferrerPolicy < Base
16
+ default_options :referrer_policy => 'strict-origin-when-cross-origin'
17
+
18
+ def call(env)
19
+ status, headers, body = @app.call(env)
20
+ headers['Referrer-Policy'] ||= options[:referrer_policy]
21
+ [status, headers, body]
22
+ end
23
+ end
24
+ end
25
+ end
@@ -10,7 +10,7 @@ module Rack
10
10
  # Only accepts unsafe HTTP requests if a given access token matches the token
11
11
  # included in the session *or* the request comes from the same origin.
12
12
  #
13
- # Compatible with Rails and rack-csrf.
13
+ # Compatible with rack-csrf.
14
14
  class RemoteToken < AuthenticityToken
15
15
  default_reaction :deny
16
16
 
@@ -14,7 +14,7 @@ module Rack
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 HTTP_ACCEPT_LANGUAGE]
17
+ :track => %w[HTTP_USER_AGENT]
18
18
 
19
19
  def accepts?(env)
20
20
  session = session env
@@ -0,0 +1,39 @@
1
+ require 'rack/protection'
2
+
3
+ module Rack
4
+ module Protection
5
+ ##
6
+ # Prevented attack:: Protects against against protocol downgrade attacks and cookie hijacking.
7
+ # Supported browsers:: all
8
+ # More infos:: https://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security
9
+ #
10
+ # browser will prevent any communications from being sent over HTTP
11
+ # to the specified domain and will instead send all communications over HTTPS.
12
+ # It also prevents HTTPS click through prompts on browsers.
13
+ #
14
+ # Options:
15
+ #
16
+ # max_age:: How long future requests to the domain should go over HTTPS; specified in seconds
17
+ # include_subdomains:: If all present and future subdomains will be HTTPS
18
+ # preload:: Allow this domain to be included in browsers HSTS preload list. See https://hstspreload.appspot.com/
19
+
20
+ class StrictTransport < Base
21
+ default_options :max_age => 31_536_000, :include_subdomains => false, :preload => false
22
+
23
+ def strict_transport
24
+ @strict_transport ||= begin
25
+ strict_transport = 'max-age=' + options[:max_age].to_s
26
+ strict_transport += '; includeSubDomains' if options[:include_subdomains]
27
+ strict_transport += '; preload' if options[:preload]
28
+ strict_transport.to_str
29
+ end
30
+ end
31
+
32
+ def call(env)
33
+ status, headers, body = @app.call(env)
34
+ headers['Strict-Transport-Security'] ||= strict_transport
35
+ [status, headers, body]
36
+ end
37
+ end
38
+ end
39
+ end
@@ -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 = '2.1.0'
15
4
  end
16
5
  end