rack-protection 1.5.3 → 2.0.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.

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