rack-protection 1.5.5 → 2.1.0

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