rack-protection 2.0.8.1

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.
@@ -0,0 +1,126 @@
1
+ require 'rack/protection'
2
+ require 'rack/utils'
3
+ require 'digest'
4
+ require 'logger'
5
+ require 'uri'
6
+
7
+ module Rack
8
+ module Protection
9
+ class Base
10
+ DEFAULT_OPTIONS = {
11
+ :reaction => :default_reaction, :logging => true,
12
+ :message => 'Forbidden', :encryptor => Digest::SHA1,
13
+ :session_key => 'rack.session', :status => 403,
14
+ :allow_empty_referrer => true,
15
+ :report_key => "protection.failed",
16
+ :html_types => %w[text/html application/xhtml text/xml application/xml]
17
+ }
18
+
19
+ attr_reader :app, :options
20
+
21
+ def self.default_options(options)
22
+ define_method(:default_options) { super().merge(options) }
23
+ end
24
+
25
+ def self.default_reaction(reaction)
26
+ alias_method(:default_reaction, reaction)
27
+ end
28
+
29
+ def default_options
30
+ DEFAULT_OPTIONS
31
+ end
32
+
33
+ def initialize(app, options = {})
34
+ @app, @options = app, default_options.merge(options)
35
+ end
36
+
37
+ def safe?(env)
38
+ %w[GET HEAD OPTIONS TRACE].include? env['REQUEST_METHOD']
39
+ end
40
+
41
+ def accepts?(env)
42
+ raise NotImplementedError, "#{self.class} implementation pending"
43
+ end
44
+
45
+ def call(env)
46
+ unless accepts? env
47
+ instrument env
48
+ result = react env
49
+ end
50
+ result or app.call(env)
51
+ end
52
+
53
+ def react(env)
54
+ result = send(options[:reaction], env)
55
+ result if Array === result and result.size == 3
56
+ end
57
+
58
+ def warn(env, message)
59
+ return unless options[:logging]
60
+ l = options[:logger] || env['rack.logger'] || ::Logger.new(env['rack.errors'])
61
+ l.warn(message)
62
+ end
63
+
64
+ def instrument(env)
65
+ return unless i = options[:instrumenter]
66
+ env['rack.protection.attack'] = self.class.name.split('::').last.downcase
67
+ i.instrument('rack.protection', env)
68
+ end
69
+
70
+ def deny(env)
71
+ warn env, "attack prevented by #{self.class}"
72
+ [options[:status], {'Content-Type' => 'text/plain'}, [options[:message]]]
73
+ end
74
+
75
+ def report(env)
76
+ warn env, "attack reported by #{self.class}"
77
+ env[options[:report_key]] = true
78
+ end
79
+
80
+ def session?(env)
81
+ env.include? options[:session_key]
82
+ end
83
+
84
+ def session(env)
85
+ return env[options[:session_key]] if session? env
86
+ fail "you need to set up a session middleware *before* #{self.class}"
87
+ end
88
+
89
+ def drop_session(env)
90
+ session(env).clear if session? env
91
+ end
92
+
93
+ def referrer(env)
94
+ ref = env['HTTP_REFERER'].to_s
95
+ return if !options[:allow_empty_referrer] and ref.empty?
96
+ URI.parse(ref).host || Request.new(env).host
97
+ rescue URI::InvalidURIError
98
+ end
99
+
100
+ def origin(env)
101
+ env['HTTP_ORIGIN'] || env['HTTP_X_ORIGIN']
102
+ end
103
+
104
+ def random_string(secure = defined? SecureRandom)
105
+ secure ? SecureRandom.hex(16) : "%032x" % rand(2**128-1)
106
+ rescue NotImplementedError
107
+ random_string false
108
+ end
109
+
110
+ def encrypt(value)
111
+ options[:encryptor].hexdigest value.to_s
112
+ end
113
+
114
+ def secure_compare(a, b)
115
+ Rack::Utils.secure_compare(a.to_s, b.to_s)
116
+ end
117
+
118
+ alias default_reaction deny
119
+
120
+ def html?(headers)
121
+ return false unless header = headers.detect { |k,v| k.downcase == 'content-type' }
122
+ options[:html_types].include? header.last[/^\w+\/\w+/]
123
+ end
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,80 @@
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: :none, script_src: "'self'",
40
+ img_src: "'self'", style_src: "'self'",
41
+ connect_src: "'self'", report_only: false
42
+
43
+ DIRECTIVES = %i(base_uri child_src connect_src default_src
44
+ font_src form_action frame_ancestors frame_src
45
+ img_src manifest_src media_src object_src
46
+ plugin_types referrer reflected_xss report_to
47
+ report_uri require_sri_for sandbox script_src
48
+ style_src worker_src).freeze
49
+
50
+ NO_ARG_DIRECTIVES = %i(block_all_mixed_content disown_opener
51
+ upgrade_insecure_requests).freeze
52
+
53
+ def csp_policy
54
+ directives = []
55
+
56
+ DIRECTIVES.each do |d|
57
+ if options.key?(d)
58
+ directives << "#{d.to_s.sub(/_/, '-')} #{options[d]}"
59
+ end
60
+ end
61
+
62
+ # Set these key values to boolean 'true' to include in policy
63
+ NO_ARG_DIRECTIVES.each do |d|
64
+ if options.key?(d) && options[d].is_a?(TrueClass)
65
+ directives << d.to_s.sub(/_/, '-')
66
+ end
67
+ end
68
+
69
+ directives.compact.sort.join('; ')
70
+ end
71
+
72
+ def call(env)
73
+ status, headers, body = @app.call(env)
74
+ header = options[:report_only] ? 'Content-Security-Policy-Report-Only' : 'Content-Security-Policy'
75
+ headers[header] ||= csp_policy if html? headers
76
+ [status, headers, body]
77
+ end
78
+ end
79
+ end
80
+ 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
@@ -0,0 +1,89 @@
1
+ require 'rack/protection'
2
+ require 'rack/utils'
3
+ require 'tempfile'
4
+
5
+ begin
6
+ require 'escape_utils'
7
+ rescue LoadError
8
+ end
9
+
10
+ module Rack
11
+ module Protection
12
+ ##
13
+ # Prevented attack:: XSS
14
+ # Supported browsers:: all
15
+ # More infos:: http://en.wikipedia.org/wiki/Cross-site_scripting
16
+ #
17
+ # Automatically escapes Rack::Request#params so they can be embedded in HTML
18
+ # or JavaScript without any further issues. Calls +html_safe+ on the escaped
19
+ # strings if defined, to avoid double-escaping in Rails.
20
+ #
21
+ # Options:
22
+ # escape:: What escaping modes to use, should be Symbol or Array of Symbols.
23
+ # Available: :html (default), :javascript, :url
24
+ class EscapedParams < Base
25
+ extend Rack::Utils
26
+
27
+ class << self
28
+ alias escape_url escape
29
+ public :escape_html
30
+ end
31
+
32
+ default_options :escape => :html,
33
+ :escaper => defined?(EscapeUtils) ? EscapeUtils : self
34
+
35
+ def initialize(*)
36
+ super
37
+
38
+ modes = Array options[:escape]
39
+ @escaper = options[:escaper]
40
+ @html = modes.include? :html
41
+ @javascript = modes.include? :javascript
42
+ @url = modes.include? :url
43
+
44
+ if @javascript and not @escaper.respond_to? :escape_javascript
45
+ fail("Use EscapeUtils for JavaScript escaping.")
46
+ end
47
+ end
48
+
49
+ def call(env)
50
+ request = Request.new(env)
51
+ get_was = handle(request.GET)
52
+ post_was = handle(request.POST) rescue nil
53
+ app.call env
54
+ ensure
55
+ request.GET.replace get_was if get_was
56
+ request.POST.replace post_was if post_was
57
+ end
58
+
59
+ def handle(hash)
60
+ was = hash.dup
61
+ hash.replace escape(hash)
62
+ was
63
+ end
64
+
65
+ def escape(object)
66
+ case object
67
+ when Hash then escape_hash(object)
68
+ when Array then object.map { |o| escape(o) }
69
+ when String then escape_string(object)
70
+ when Tempfile then object
71
+ else nil
72
+ end
73
+ end
74
+
75
+ def escape_hash(hash)
76
+ hash = hash.dup
77
+ hash.each { |k,v| hash[k] = escape(v) }
78
+ hash
79
+ end
80
+
81
+ def escape_string(str)
82
+ str = @escaper.escape_url(str) if @url
83
+ str = @escaper.escape_html(str) if @html
84
+ str = @escaper.escape_javascript(str) if @javascript
85
+ str
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,23 @@
1
+ require 'rack/protection'
2
+
3
+ module Rack
4
+ module Protection
5
+ ##
6
+ # Prevented attack:: CSRF
7
+ # Supported browsers:: all
8
+ # More infos:: http://en.wikipedia.org/wiki/Cross-site_request_forgery
9
+ #
10
+ # Only accepts submitted forms if a given access token matches the token
11
+ # included in the session. Does not expect such a token from Ajax request.
12
+ #
13
+ # This middleware is not used when using the Rack::Protection collection,
14
+ # since it might be a security issue, depending on your application
15
+ #
16
+ # Compatible with rack-csrf.
17
+ class FormToken < AuthenticityToken
18
+ def accepts?(env)
19
+ env["HTTP_X_REQUESTED_WITH"] == "XMLHttpRequest" or super
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,37 @@
1
+ require 'rack/protection'
2
+
3
+ module Rack
4
+ module Protection
5
+ ##
6
+ # Prevented attack:: Clickjacking
7
+ # Supported browsers:: Internet Explorer 8, Firefox 3.6.9, Opera 10.50,
8
+ # Safari 4.0, Chrome 4.1.249.1042 and later
9
+ # More infos:: https://developer.mozilla.org/en/The_X-FRAME-OPTIONS_response_header
10
+ #
11
+ # Sets X-Frame-Options header to tell the browser avoid embedding the page
12
+ # in a frame.
13
+ #
14
+ # Options:
15
+ #
16
+ # frame_options:: Defines who should be allowed to embed the page in a
17
+ # frame. Use :deny to forbid any embedding, :sameorigin
18
+ # to allow embedding from the same origin (default).
19
+ class FrameOptions < Base
20
+ default_options :frame_options => :sameorigin
21
+
22
+ def frame_options
23
+ @frame_options ||= begin
24
+ frame_options = options[:frame_options]
25
+ frame_options = options[:frame_options].to_s.upcase unless frame_options.respond_to? :to_str
26
+ frame_options.to_str
27
+ end
28
+ end
29
+
30
+ def call(env)
31
+ status, headers, body = @app.call(env)
32
+ headers['X-Frame-Options'] ||= frame_options if html? headers
33
+ [status, headers, body]
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,40 @@
1
+ require 'rack/protection'
2
+
3
+ module Rack
4
+ module Protection
5
+ ##
6
+ # Prevented attack:: CSRF
7
+ # Supported browsers:: Google Chrome 2, Safari 4 and later
8
+ # More infos:: http://en.wikipedia.org/wiki/Cross-site_request_forgery
9
+ # http://tools.ietf.org/html/draft-abarth-origin
10
+ #
11
+ # Does not accept unsafe HTTP requests when value of Origin HTTP request header
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.
19
+ class HttpOrigin < Base
20
+ DEFAULT_PORTS = { 'http' => 80, 'https' => 443, 'coffee' => 80 }
21
+ default_reaction :deny
22
+ default_options :allow_if => nil
23
+
24
+ def base_url(env)
25
+ request = Rack::Request.new(env)
26
+ port = ":#{request.port}" unless request.port == DEFAULT_PORTS[request.scheme]
27
+ "#{request.scheme}://#{request.host}#{port}"
28
+ end
29
+
30
+ def accepts?(env)
31
+ return true if safe? env
32
+ return true unless origin = env['HTTP_ORIGIN']
33
+ return true if base_url(env) == origin
34
+ return true if options[:allow_if] && options[:allow_if].call(env)
35
+ Array(options[:origin_whitelist]).include? origin
36
+ end
37
+
38
+ end
39
+ end
40
+ end