rack-protection 0.1.0 → 1.5.5

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 2b7d78da301d9f7fc81ae73e46a389c2b8ce10ab8121f169fd760018ac506d47
4
+ data.tar.gz: a91bd28f8624f325d6714262ac11d83e0a347405f14e600780cb1bfd846e5b34
5
+ SHA512:
6
+ metadata.gz: 0c5de92c0283313c00d50c1f9a219c808ad587caabff81c4d1530abd8f0e7d9c0f3753ad9bab7c06a29ce97b2a717fddc04ced642adff058f3431419286e4da6
7
+ data.tar.gz: d3bf5830bf30475871b73ba54ee38f962bd93c2e1f420b59b649d6dcb7f97d89f091d3add3f692ba847ffe5f5c6cade665d522c86220087d977fb3706e41bd58
data/README.md CHANGED
@@ -43,12 +43,14 @@ Prevented by:
43
43
  * `Rack::Protection::JsonCsrf`
44
44
  * `Rack::Protection::RemoteReferrer` (not included by `use Rack::Protection`)
45
45
  * `Rack::Protection::RemoteToken`
46
+ * `Rack::Protection::HttpOrigin`
47
+
46
48
  ## Cross Site Scripting
47
49
 
48
50
  Prevented by:
49
51
 
50
- * `Rack::Protection::EscapedParams`
51
- * `Rack::Protection::XssHeader` (Internet Explorer only)
52
+ * `Rack::Protection::EscapedParams` (not included by `use Rack::Protection`)
53
+ * `Rack::Protection::XSSHeader` (Internet Explorer only)
52
54
 
53
55
  ## Clickjacking
54
56
 
@@ -70,7 +72,6 @@ Prevented by:
70
72
 
71
73
  ## IP Spoofing
72
74
 
73
-
74
75
  Prevented by:
75
76
 
76
77
  * `Rack::Protection::IPSpoofing`
@@ -78,3 +79,12 @@ Prevented by:
78
79
  # Installation
79
80
 
80
81
  gem install rack-protection
82
+
83
+ # Instrumentation
84
+
85
+ Instrumentation is enabled by passing in an instrumenter as an option.
86
+ ```
87
+ use Rack::Protection, instrumenter: ActiveSupport::Notifications
88
+ ```
89
+
90
+ The instrumenter is passed a namespace (String) and environment (Hash). The namespace is 'rack.protection' and the attack type can be obtained from the environment key 'rack.protection.attack'.
data/Rakefile CHANGED
@@ -1,3 +1,4 @@
1
+ # encoding: utf-8
1
2
  $LOAD_PATH.unshift File.expand_path('../lib', __FILE__)
2
3
 
3
4
  begin
@@ -13,14 +14,19 @@ task(:spec) { ruby '-S rspec spec' }
13
14
  desc "generate gemspec"
14
15
  task 'rack-protection.gemspec' do
15
16
  require 'rack/protection/version'
16
- content = File.read 'rack-protection.gemspec'
17
+ content = File.binread 'rack-protection.gemspec'
17
18
 
19
+ # fetch data
18
20
  fields = {
19
- :authors => `git shortlog -sn`.scan(/[^\d\s].*/),
20
- :email => `git shortlog -sne`.scan(/[^<]+@[^>]+/),
21
- :files => `git ls-files`.split("\n").reject { |f| f =~ /^(\.|Gemfile)/ }
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
24
  }
23
25
 
26
+ # double email :(
27
+ fields[:email].delete("konstantin.haase@gmail.com")
28
+
29
+ # insert data
24
30
  fields.each do |field, values|
25
31
  updated = " s.#{field} = ["
26
32
  updated << values.map { |v| "\n %p" % v }.join(',')
@@ -28,7 +34,12 @@ task 'rack-protection.gemspec' do
28
34
  content.sub!(/ s\.#{field} = \[\n( .*\n)* \]/, updated)
29
35
  end
30
36
 
37
+ # set version
31
38
  content.sub! /(s\.version.*=\s+).*/, "\\1\"#{Rack::Protection::VERSION}\""
39
+
40
+ # escape unicode
41
+ content.gsub!(/./) { |c| c.bytesize > 1 ? "\\u{#{c.codepoints.first.to_s(16)}}" : c }
42
+
32
43
  File.open('rack-protection.gemspec', 'w') { |f| f << content }
33
44
  end
34
45
 
@@ -11,13 +11,20 @@ module Rack
11
11
  # included in the session.
12
12
  #
13
13
  # Compatible with Rails and rack-csrf.
14
+ #
15
+ # Options:
16
+ #
17
+ # authenticity_param: Defines the param's name that should contain the token on a request.
18
+ #
14
19
  class AuthenticityToken < Base
20
+ default_options :authenticity_param => 'authenticity_token'
21
+
15
22
  def accepts?(env)
16
- return true if safe? env
17
23
  session = session env
18
24
  token = session[:csrf] ||= session['_csrf_token'] || random_string
19
- env['HTTP_X_CSRF_TOKEN'] == token or
20
- Request.new(env).params['authenticity_token'] == token
25
+ 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)
21
28
  end
22
29
  end
23
30
  end
@@ -10,7 +10,9 @@ module Rack
10
10
  :reaction => :default_reaction, :logging => true,
11
11
  :message => 'Forbidden', :encryptor => Digest::SHA1,
12
12
  :session_key => 'rack.session', :status => 403,
13
- :allow_empty_referrer => true
13
+ :allow_empty_referrer => true,
14
+ :report_key => "protection.failed",
15
+ :html_types => %w[text/html application/xhtml]
14
16
  }
15
17
 
16
18
  attr_reader :app, :options
@@ -41,7 +43,7 @@ module Rack
41
43
 
42
44
  def call(env)
43
45
  unless accepts? env
44
- warn env, "attack prevented by #{self.class}"
46
+ instrument env
45
47
  result = react env
46
48
  end
47
49
  result or app.call(env)
@@ -58,10 +60,22 @@ module Rack
58
60
  l.warn(message)
59
61
  end
60
62
 
63
+ def instrument(env)
64
+ return unless i = options[:instrumenter]
65
+ env['rack.protection.attack'] = self.class.name.split('::').last.downcase
66
+ i.instrument('rack.protection', env)
67
+ end
68
+
61
69
  def deny(env)
70
+ warn env, "attack prevented by #{self.class}"
62
71
  [options[:status], {'Content-Type' => 'text/plain'}, [options[:message]]]
63
72
  end
64
73
 
74
+ def report(env)
75
+ warn env, "attack reported by #{self.class}"
76
+ env[options[:report_key]] = true
77
+ end
78
+
65
79
  def session?(env)
66
80
  env.include? options[:session_key]
67
81
  end
@@ -79,11 +93,16 @@ module Rack
79
93
  ref = env['HTTP_REFERER'].to_s
80
94
  return if !options[:allow_empty_referrer] and ref.empty?
81
95
  URI.parse(ref).host || Request.new(env).host
96
+ rescue URI::InvalidURIError
97
+ end
98
+
99
+ def origin(env)
100
+ env['HTTP_ORIGIN'] || env['HTTP_X_ORIGIN']
82
101
  end
83
102
 
84
103
  def random_string(secure = defined? SecureRandom)
85
- secure ? SecureRandom.hex(32) : "%032x" % rand(2**128-1)
86
- rescue NotImpelentedError
104
+ secure ? SecureRandom.hex(16) : "%032x" % rand(2**128-1)
105
+ rescue NotImplementedError
87
106
  random_string false
88
107
  end
89
108
 
@@ -91,7 +110,36 @@ module Rack
91
110
  options[:encryptor].hexdigest value.to_s
92
111
  end
93
112
 
113
+ # The implementations of secure_compare and bytesize are taken from
114
+ # Rack::Utils to be able to support rack older than XXXX.
115
+ 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
135
+ end
136
+
94
137
  alias default_reaction deny
138
+
139
+ def html?(headers)
140
+ return false unless header = headers.detect { |k,v| k.downcase == 'content-type' }
141
+ options[:html_types].include? header.last[/^\w+\/\w+/]
142
+ end
95
143
  end
96
144
  end
97
145
  end
@@ -1,5 +1,10 @@
1
1
  require 'rack/protection'
2
- require 'escape_utils'
2
+ require 'rack/utils'
3
+
4
+ begin
5
+ require 'escape_utils'
6
+ rescue LoadError
7
+ end
3
8
 
4
9
  module Rack
5
10
  module Protection
@@ -16,14 +21,28 @@ module Rack
16
21
  # escape:: What escaping modes to use, should be Symbol or Array of Symbols.
17
22
  # Available: :html (default), :javascript, :url
18
23
  class EscapedParams < Base
19
- default_options :escape => :html
24
+ extend Rack::Utils
25
+
26
+ class << self
27
+ alias escape_url escape
28
+ public :escape_html
29
+ end
30
+
31
+ default_options :escape => :html,
32
+ :escaper => defined?(EscapeUtils) ? EscapeUtils : self
20
33
 
21
34
  def initialize(*)
22
35
  super
23
- modes = Array options[:escape]
24
- code = "def self.escape_string(str) %s end"
25
- modes.each { |m| code %= "EscapeUtils.escape_#{m}(%s)"}
26
- eval code % 'str'
36
+
37
+ modes = Array options[:escape]
38
+ @escaper = options[:escaper]
39
+ @html = modes.include? :html
40
+ @javascript = modes.include? :javascript
41
+ @url = modes.include? :url
42
+
43
+ if @javascript and not @escaper.respond_to? :escape_javascript
44
+ fail("Use EscapeUtils for JavaScript escaping.")
45
+ end
27
46
  end
28
47
 
29
48
  def call(env)
@@ -32,7 +51,7 @@ module Rack
32
51
  post_was = handle(request.POST) rescue nil
33
52
  app.call env
34
53
  ensure
35
- request.GET.replace get_was
54
+ request.GET.replace get_was if get_was
36
55
  request.POST.replace post_was if post_was
37
56
  end
38
57
 
@@ -47,7 +66,7 @@ module Rack
47
66
  when Hash then escape_hash(object)
48
67
  when Array then object.map { |o| escape(o) }
49
68
  when String then escape_string(object)
50
- else raise ArgumentError, "cannot escape #{object.inspect}"
69
+ else nil
51
70
  end
52
71
  end
53
72
 
@@ -56,6 +75,13 @@ module Rack
56
75
  hash.each { |k,v| hash[k] = escape(v) }
57
76
  hash
58
77
  end
78
+
79
+ def escape_string(str)
80
+ str = @escaper.escape_url(str) if @url
81
+ str = @escaper.escape_html(str) if @html
82
+ str = @escaper.escape_javascript(str) if @javascript
83
+ str
84
+ end
59
85
  end
60
86
  end
61
87
  end
@@ -16,10 +16,21 @@ module Rack
16
16
  # frame_options:: Defines who should be allowed to embed the page in a
17
17
  # frame. Use :deny to forbid any embedding, :sameorigin
18
18
  # to allow embedding from the same origin (default).
19
- class FrameOptions < XSSHeader
19
+ class FrameOptions < Base
20
20
  default_options :frame_options => :sameorigin
21
- def header
22
- { 'X-Frame-Options' => options[:frame_options].to_s }
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]
23
34
  end
24
35
  end
25
36
  end
@@ -0,0 +1,32 @@
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
+ class HttpOrigin < Base
14
+ DEFAULT_PORTS = { 'http' => 80, 'https' => 443, 'coffee' => 80 }
15
+ default_reaction :deny
16
+
17
+ def base_url(env)
18
+ request = Rack::Request.new(env)
19
+ port = ":#{request.port}" unless request.port == DEFAULT_PORTS[request.scheme]
20
+ "#{request.scheme}://#{request.host}#{port}"
21
+ end
22
+
23
+ def accepts?(env)
24
+ return true if safe? env
25
+ return true unless origin = env['HTTP_ORIGIN']
26
+ return true if base_url(env) == origin
27
+ Array(options[:origin_whitelist]).include? origin
28
+ end
29
+
30
+ end
31
+ end
32
+ end
@@ -13,7 +13,7 @@ module Rack
13
13
 
14
14
  def accepts?(env)
15
15
  return true unless env.include? 'HTTP_X_FORWARDED_FOR'
16
- ips = env['HTTP_X_FORWARDED_FOR'].split /\s*,\s*/
16
+ ips = env['HTTP_X_FORWARDED_FOR'].split(/\s*,\s*/)
17
17
  return false if env.include? 'HTTP_CLIENT_IP' and not ips.include? env['HTTP_CLIENT_IP']
18
18
  return false if env.include? 'HTTP_X_REAL_IP' and not ips.include? env['HTTP_X_REAL_IP']
19
19
  true
@@ -7,18 +7,28 @@ module Rack
7
7
  # Supported browsers:: all
8
8
  # More infos:: http://flask.pocoo.org/docs/security/#json-security
9
9
  #
10
- # JSON GET APIs are volnurable to being embedded as JavaScript while the
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
13
  class JsonCsrf < Base
14
- default_reaction :deny
14
+ alias react deny
15
15
 
16
16
  def call(env)
17
+ request = Request.new(env)
17
18
  status, headers, body = app.call(env)
18
- if headers['Content-Type'].to_s.split(';', 2).first.strip == 'application/json'
19
- result = react(env) if referrer(env) != Request.new(env).host
19
+
20
+ if has_vector? request, headers
21
+ warn env, "attack prevented by #{self.class}"
22
+ react(env) or [status, headers, body]
23
+ else
24
+ [status, headers, body]
20
25
  end
21
- result or [status, headers, body]
26
+ end
27
+
28
+ def has_vector?(request, headers)
29
+ return false if request.xhr?
30
+ return false unless headers['Content-Type'].to_s.split(';', 2).first =~ /^\s*application\/json\s*$/
31
+ origin(request.env).nil? and referrer(request.env) != request.host
22
32
  end
23
33
  end
24
34
  end
@@ -12,17 +12,38 @@ module Rack
12
12
  class PathTraversal < Base
13
13
  def call(env)
14
14
  path_was = env["PATH_INFO"]
15
- env["PATH_INFO"] = cleanup path_was
15
+ env["PATH_INFO"] = cleanup path_was if path_was && !path_was.empty?
16
16
  app.call env
17
17
  ensure
18
18
  env["PATH_INFO"] = path_was
19
19
  end
20
20
 
21
21
  def cleanup(path)
22
- return cleanup("/" << path)[1..-1] unless path[0] == ?/
23
- escaped = ::File.expand_path path.gsub('%2e', '.').gsub('%2f', '/')
24
- escaped << '/' if escaped[-1] != ?/ and path =~ /\/\.{0,2}$/
25
- escaped.gsub /\/\/+/, '/'
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
34
+
35
+ parts = []
36
+ unescaped = path.gsub(/%2e/i, dot).gsub(/%2f/i, slash).gsub(/%5c/i, backslash)
37
+ unescaped = unescaped.gsub(backslash, slash)
38
+
39
+ unescaped.split(slash).each do |part|
40
+ next if part.empty? or part == dot
41
+ part == '..' ? parts.pop : parts << part
42
+ end
43
+
44
+ cleaned = slash + parts.join(slash)
45
+ cleaned << slash if parts.any? and unescaped =~ %r{/\.{0,2}$}
46
+ cleaned
26
47
  end
27
48
  end
28
49
  end
@@ -9,9 +9,6 @@ module Rack
9
9
  #
10
10
  # Does not accept unsafe HTTP requests if the Referer [sic] header is set to
11
11
  # a different host.
12
- #
13
- # Combine with NoReferrer to also block remote requests from non-HTTP pages
14
- # (FTP/HTTPS/...).
15
12
  class RemoteReferrer < Base
16
13
  default_reaction :deny
17
14
 
@@ -9,13 +9,12 @@ module Rack
9
9
  #
10
10
  # Tracks request properties like the user agent in the session and empties
11
11
  # the session if those properties change. This essentially prevents attacks
12
- # from Firesheep. Since all headers taken into consideration might be
13
- # spoofed, too, this will not prevent all hijacking attempts.
12
+ # from Firesheep. Since all headers taken into consideration can be
13
+ # spoofed, too, this will not prevent determined hijacking attempts.
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_ENCODING HTTP_ACCEPT_LANGUAGE
18
- HTTP_VERSION]
17
+ :track => %w[HTTP_USER_AGENT HTTP_ACCEPT_LANGUAGE]
19
18
 
20
19
  def accepts?(env)
21
20
  session = session env
@@ -29,7 +28,8 @@ module Rack
29
28
  end
30
29
 
31
30
  def encrypt(value)
32
- options[:encrypt_tracking] ? super(value) : value.to_s
31
+ value = value.to_s.downcase
32
+ options[:encrypt_tracking] ? super(value) : value
33
33
  end
34
34
  end
35
35
  end
@@ -4,41 +4,13 @@ module Rack
4
4
  VERSION
5
5
  end
6
6
 
7
- module VERSION
8
- extend Comparable
7
+ SIGNATURE = [1, 5, 5]
8
+ VERSION = SIGNATURE.join('.')
9
9
 
10
- MAJOR = 0
11
- MINOR = 1
12
- TINY = 0
13
- SIGNATURE = [MAJOR, MINOR, TINY]
14
- STRING = SIGNATURE.join '.'
15
-
16
- def self.major; MAJOR end
17
- def self.minor; MINOR end
18
- def self.tiny; TINY end
19
- def self.to_s; STRING end
20
-
21
- def self.hash
22
- STRING.hash
23
- end
24
-
25
- def self.<=>(other)
26
- other = other.split('.').map { |i| i.to_i } if other.respond_to? :split
27
- SIGNATURE <=> Array(other)
28
- end
29
-
30
- def self.inspect
31
- STRING.inspect
32
- end
33
-
34
- def self.respond_to?(meth, *)
35
- meth.to_s !~ /^__|^to_str$/ and STRING.respond_to? meth unless super
36
- end
37
-
38
- def self.method_missing(meth, *args, &block)
39
- return super unless STRING.respond_to?(meth)
40
- STRING.send(meth, *args, &block)
41
- end
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)
42
14
  end
43
15
  end
44
16
  end
@@ -12,15 +12,13 @@ module Rack
12
12
  # Options:
13
13
  # xss_mode:: How the browser should prevent the attack (default: :block)
14
14
  class XSSHeader < Base
15
- default_options :xss_mode => :block
16
-
17
- def header
18
- { 'X-XSS-Protection' => "1; mode=#{options[:xss_mode]}" }
19
- end
15
+ default_options :xss_mode => :block, :nosniff => true
20
16
 
21
17
  def call(env)
22
18
  status, headers, body = @app.call(env)
23
- [status, header.merge(headers), body]
19
+ headers['X-XSS-Protection'] ||= "1; mode=#{options[:xss_mode]}" if html? headers
20
+ headers['X-Content-Type-Options'] ||= 'nosniff' if options[:nosniff]
21
+ [status, headers, body]
24
22
  end
25
23
  end
26
24
  end
@@ -8,6 +8,7 @@ module Rack
8
8
  autoload :EscapedParams, 'rack/protection/escaped_params'
9
9
  autoload :FormToken, 'rack/protection/form_token'
10
10
  autoload :FrameOptions, 'rack/protection/frame_options'
11
+ autoload :HttpOrigin, 'rack/protection/http_origin'
11
12
  autoload :IPSpoofing, 'rack/protection/ip_spoofing'
12
13
  autoload :JsonCsrf, 'rack/protection/json_csrf'
13
14
  autoload :PathTraversal, 'rack/protection/path_traversal'
@@ -19,15 +20,19 @@ module Rack
19
20
  def self.new(app, options = {})
20
21
  # does not include: RemoteReferrer, AuthenticityToken and FormToken
21
22
  except = Array options[:except]
23
+ use_these = Array options[:use]
22
24
  Rack::Builder.new do
23
- use EscapedParams, options unless except.include? :escaped_params
24
- use FrameOptions, options unless except.include? :frame_options
25
- use IPSpoofing, options unless except.include? :ip_spoofing
26
- use JsonCsrf, options unless except.include? :json_csrf
27
- use PathTraversal, options unless except.include? :path_traversal
28
- use RemoteToken, options unless except.include? :remote_token
29
- use SessionHijacking, options unless except.include? :session_hijacking
30
- use XSSHeader, options unless except.include? :xss_header
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
31
36
  run app
32
37
  end.to_app
33
38
  end