rack-protection 1.5.3 → 2.0.0

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
2
  SHA1:
3
- metadata.gz: 738b46a37db596fd6ab75ccccfcf98b8530684d5
4
- data.tar.gz: ba76d3a2e8e5f5ec8493acf43980325a5a2bfb55
3
+ metadata.gz: 60b24d006884c214484d2d6275ddfd9a09719fe6
4
+ data.tar.gz: b43b08983d4fc1fcf5525d28e0b704e49ee93a25
5
5
  SHA512:
6
- metadata.gz: 3c88e6d4d2bcb83aa35327db0bf8d1ef7e0057579573e305958a99cdb642bffab66009e73404322be636bc3860c0acbd58fc6c15a6dda8d55948713ef28fbae4
7
- data.tar.gz: 651bf843d47d99accab655195673ae835d266602845edb8fadd913c7bff8677636c0b2db825ea0e087309b6d62f89035d503eccf6e698c2d11c625150eccb111
6
+ metadata.gz: 926c434a0da749f9e615786c4600d7f3a933454ba179b4f3671d3abeb65d843281b95038258edaac3f01ae447c032aa8844141de0a399fd5447e6cbab12deac6
7
+ data.tar.gz: 64892fdb92b20c537ffebbbc00d374e4d0bd07cf37dab25c21c95676b371477356c7ba3a52945e0f9a84b0db40bbd3fc964aa21a8525ed2bfac4dd3be204817f
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/License CHANGED
@@ -1,4 +1,7 @@
1
- Copyright (c) 2011 Konstantin Haase
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2011-2017 Konstantin Haase
4
+ Copyright (c) 2015-2017 Zachary Scott
2
5
 
3
6
  Permission is hereby granted, free of charge, to any person obtaining
4
7
  a copy of this software and associated documentation files (the
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
@@ -11,6 +11,25 @@ end
11
11
  desc "run specs"
12
12
  task(:spec) { ruby '-S rspec spec' }
13
13
 
14
+ namespace :doc do
15
+ task :readmes do
16
+ Dir.glob 'lib/rack/protection/*.rb' do |file|
17
+ excluded_files = %w[lib/rack/protection/base.rb lib/rack/protection/version.rb]
18
+ next if excluded_files.include?(file)
19
+ doc = File.read(file)[/^ module Protection(\n)+( #[^\n]*\n)*/m].scan(/^ *#(?!#) ?(.*)\n/).join("\n")
20
+ file = "doc/#{file[4..-4].tr("/_", "-")}.rdoc"
21
+ Dir.mkdir "doc" unless File.directory? "doc"
22
+ puts "writing #{file}"
23
+ File.open(file, "w") { |f| f << doc }
24
+ end
25
+ end
26
+
27
+ task :all => [:readmes]
28
+ end
29
+
30
+ desc "generate documentation"
31
+ task :doc => 'doc:all'
32
+
14
33
  desc "generate gemspec"
15
34
  task 'rack-protection.gemspec' do
16
35
  require 'rack/protection/version'
@@ -19,13 +38,10 @@ task 'rack-protection.gemspec' do
19
38
  # fetch data
20
39
  fields = {
21
40
  :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)/ }
41
+ :email => ["mail@zzak.io", "konstantin.haase@gmail.com"],
42
+ :files => %w(License README.md Rakefile Gemfile rack-protection.gemspec) + Dir['lib/**/*']
24
43
  }
25
44
 
26
- # double email :(
27
- fields[:email].delete("konstantin.haase@gmail.com")
28
-
29
45
  # insert data
30
46
  fields.each do |field, values|
31
47
  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
@@ -10,21 +12,120 @@ module Rack
10
12
  # Only accepts unsafe HTTP requests if a given access token matches the token
11
13
  # included in the session.
12
14
  #
13
- # Compatible with Rails and rack-csrf.
15
+ # Compatible with rack-csrf.
14
16
  #
15
17
  # Options:
16
18
  #
17
19
  # authenticity_param: Defines the param's name that should contain the token on a request.
18
- #
19
20
  class AuthenticityToken < Base
20
- default_options :authenticity_param => 'authenticity_token'
21
+ TOKEN_LENGTH = 32
22
+
23
+ default_options :authenticity_param => 'authenticity_token',
24
+ :allow_if => nil
25
+
26
+ def self.token(session)
27
+ self.new(nil).mask_authenticity_token(session)
28
+ end
29
+
30
+ def self.random_token
31
+ SecureRandom.base64(TOKEN_LENGTH)
32
+ end
21
33
 
22
34
  def accepts?(env)
23
35
  session = session env
24
- token = session[:csrf] ||= session['_csrf_token'] || random_string
36
+ set_token(session)
37
+
25
38
  safe?(env) ||
26
- env['HTTP_X_CSRF_TOKEN'] == token ||
27
- Request.new(env).params[options[:authenticity_param]] == token
39
+ valid_token?(session, env['HTTP_X_CSRF_TOKEN']) ||
40
+ valid_token?(session, Request.new(env).params[options[:authenticity_param]]) ||
41
+ ( options[:allow_if] && options[:allow_if].call(env) )
42
+ end
43
+
44
+ def mask_authenticity_token(session)
45
+ token = set_token(session)
46
+ mask_token(token)
47
+ end
48
+
49
+ private
50
+
51
+ def set_token(session)
52
+ session[:csrf] ||= self.class.random_token
53
+ end
54
+
55
+ # Checks the client's masked token to see if it matches the
56
+ # session token.
57
+ def valid_token?(session, token)
58
+ return false if token.nil? || token.empty?
59
+
60
+ begin
61
+ token = decode_token(token)
62
+ rescue ArgumentError # encoded_masked_token is invalid Base64
63
+ return false
64
+ end
65
+
66
+ # See if it's actually a masked token or not. We should be able
67
+ # to handle any unmasked tokens that we've issued without error.
68
+
69
+ if unmasked_token?(token)
70
+ compare_with_real_token token, session
71
+
72
+ elsif masked_token?(token)
73
+ token = unmask_token(token)
74
+
75
+ compare_with_real_token token, session
76
+
77
+ else
78
+ false # Token is malformed
79
+ end
80
+ end
81
+
82
+ # Creates a masked version of the authenticity token that varies
83
+ # on each request. The masking is used to mitigate SSL attacks
84
+ # like BREACH.
85
+ def mask_token(token)
86
+ token = decode_token(token)
87
+ one_time_pad = SecureRandom.random_bytes(token.length)
88
+ encrypted_token = xor_byte_strings(one_time_pad, token)
89
+ masked_token = one_time_pad + encrypted_token
90
+ encode_token(masked_token)
91
+ end
92
+
93
+ # Essentially the inverse of +mask_token+.
94
+ def unmask_token(masked_token)
95
+ # Split the token into the one-time pad and the encrypted
96
+ # value and decrypt it
97
+ token_length = masked_token.length / 2
98
+ one_time_pad = masked_token[0...token_length]
99
+ encrypted_token = masked_token[token_length..-1]
100
+ xor_byte_strings(one_time_pad, encrypted_token)
101
+ end
102
+
103
+ def unmasked_token?(token)
104
+ token.length == TOKEN_LENGTH
105
+ end
106
+
107
+ def masked_token?(token)
108
+ token.length == TOKEN_LENGTH * 2
109
+ end
110
+
111
+ def compare_with_real_token(token, session)
112
+ secure_compare(token, real_token(session))
113
+ end
114
+
115
+ def real_token(session)
116
+ decode_token(session[:csrf])
117
+ end
118
+
119
+ def encode_token(token)
120
+ Base64.strict_encode64(token)
121
+ end
122
+
123
+ def decode_token(token)
124
+ Base64.strict_decode64(token)
125
+ end
126
+
127
+ def xor_byte_strings(s1, s2)
128
+ s1.bytes.zip(s2.bytes).map { |(c1,c2)| c1 ^ c2 }.pack('c*')
28
129
  end
29
130
  end
30
131
  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,6 +111,10 @@ module Rack
110
111
  options[:encryptor].hexdigest value.to_s
111
112
  end
112
113
 
114
+ def secure_compare(a, b)
115
+ Rack::Utils.secure_compare(a.to_s, b.to_s)
116
+ end
117
+
113
118
  alias default_reaction deny
114
119
 
115
120
  def html?(headers)
@@ -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