rack-protection 2.0.7

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,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 8409e3948b276eede337038b10bb8dc59add80afbfa28fbcd2160f9f52670b82
4
+ data.tar.gz: 1d5485791b331fad229c63535c5c94761dc839194a90ee22811a9eb6a5e6be40
5
+ SHA512:
6
+ metadata.gz: 1bb5f7e556b1fdf46dd029c4f620e7062527d4b3280d0ce80e43552d5fd260f3445a4411f0f680689d215cc4532cb1e3f5b5cf64eae91a8411daccabeedcf557
7
+ data.tar.gz: 3108b981b3ada3ea6959f494f08fc6ea1c15e0bf9f269cf5d8a4c82103f9cdcf7d858fbf864eb95ab98fa9eaf041aef4d8f352d05e55167879563e7500896967
data/Gemfile ADDED
@@ -0,0 +1,13 @@
1
+ source "https://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 ADDED
@@ -0,0 +1,23 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2011-2017 Konstantin Haase
4
+ Copyright (c) 2015-2017 Zachary Scott
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining
7
+ a copy of this software and associated documentation files (the
8
+ 'Software'), to deal in the Software without restriction, including
9
+ without limitation the rights to use, copy, modify, merge, publish,
10
+ distribute, sublicense, and/or sell copies of the Software, and to
11
+ permit persons to whom the Software is furnished to do so, subject to
12
+ the following conditions:
13
+
14
+ The above copyright notice and this permission notice shall be
15
+ included in all copies or substantial portions of the Software.
16
+
17
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
18
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
19
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
20
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
21
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
22
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
23
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,118 @@
1
+ # Rack::Protection
2
+
3
+ This gem protects against typical web attacks.
4
+ Should work for all Rack apps, including Rails.
5
+
6
+ # Usage
7
+
8
+ Use all protections you probably want to use:
9
+
10
+ ``` ruby
11
+ # config.ru
12
+ require 'rack/protection'
13
+ use Rack::Protection
14
+ run MyApp
15
+ ```
16
+
17
+ Skip a single protection middleware:
18
+
19
+ ``` ruby
20
+ # config.ru
21
+ require 'rack/protection'
22
+ use Rack::Protection, :except => :path_traversal
23
+ run MyApp
24
+ ```
25
+
26
+ Use a single protection middleware:
27
+
28
+ ``` ruby
29
+ # config.ru
30
+ require 'rack/protection'
31
+ use Rack::Protection::AuthenticityToken
32
+ run MyApp
33
+ ```
34
+
35
+ # Prevented Attacks
36
+
37
+ ## Cross Site Request Forgery
38
+
39
+ Prevented by:
40
+
41
+ * [`Rack::Protection::AuthenticityToken`][authenticity-token] (not included by `use Rack::Protection`)
42
+ * [`Rack::Protection::FormToken`][form-token] (not included by `use Rack::Protection`)
43
+ * [`Rack::Protection::JsonCsrf`][json-csrf]
44
+ * [`Rack::Protection::RemoteReferrer`][remote-referrer] (not included by `use Rack::Protection`)
45
+ * [`Rack::Protection::RemoteToken`][remote-token]
46
+ * [`Rack::Protection::HttpOrigin`][http-origin]
47
+
48
+ ## Cross Site Scripting
49
+
50
+ Prevented by:
51
+
52
+ * [`Rack::Protection::EscapedParams`][escaped-params] (not included by `use Rack::Protection`)
53
+ * [`Rack::Protection::XSSHeader`][xss-header] (Internet Explorer and Chrome only)
54
+ * [`Rack::Protection::ContentSecurityPolicy`][content-security-policy]
55
+
56
+ ## Clickjacking
57
+
58
+ Prevented by:
59
+
60
+ * [`Rack::Protection::FrameOptions`][frame-options]
61
+
62
+ ## Directory Traversal
63
+
64
+ Prevented by:
65
+
66
+ * [`Rack::Protection::PathTraversal`][path-traversal]
67
+
68
+ ## Session Hijacking
69
+
70
+ Prevented by:
71
+
72
+ * [`Rack::Protection::SessionHijacking`][session-hijacking]
73
+
74
+ ## Cookie Tossing
75
+
76
+ Prevented by:
77
+ * [`Rack::Protection::CookieTossing`][cookie-tossing] (not included by `use Rack::Protection`)
78
+
79
+ ## IP Spoofing
80
+
81
+ Prevented by:
82
+
83
+ * [`Rack::Protection::IPSpoofing`][ip-spoofing]
84
+
85
+ ## Helps to protect against protocol downgrade attacks and cookie hijacking
86
+
87
+ Prevented by:
88
+
89
+ * [`Rack::Protection::StrictTransport`][strict-transport] (not included by `use Rack::Protection`)
90
+
91
+ # Installation
92
+
93
+ gem install rack-protection
94
+
95
+ # Instrumentation
96
+
97
+ Instrumentation is enabled by passing in an instrumenter as an option.
98
+ ```
99
+ use Rack::Protection, instrumenter: ActiveSupport::Notifications
100
+ ```
101
+
102
+ 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'.
103
+
104
+ [authenticity-token]: http://www.sinatrarb.com/protection/authenticity_token
105
+ [content-security-policy]: http://www.sinatrarb.com/protection/content_security_policy
106
+ [cookie-tossing]: http://www.sinatrarb.com/protection/cookie_tossing
107
+ [escaped-params]: http://www.sinatrarb.com/protection/escaped_params
108
+ [form-token]: http://www.sinatrarb.com/protection/form_token
109
+ [frame-options]: http://www.sinatrarb.com/protection/frame_options
110
+ [http-origin]: http://www.sinatrarb.com/protection/http_origin
111
+ [ip-spoofing]: http://www.sinatrarb.com/protection/ip_spoofing
112
+ [json-csrf]: http://www.sinatrarb.com/protection/json_csrf
113
+ [path-traversal]: http://www.sinatrarb.com/protection/path_traversal
114
+ [remote-referrer]: http://www.sinatrarb.com/protection/remote_referrer
115
+ [remote-token]: http://www.sinatrarb.com/protection/remote_token
116
+ [session-hijacking]: http://www.sinatrarb.com/protection/session_hijacking
117
+ [strict-transport]: http://www.sinatrarb.com/protection/strict_transport
118
+ [xss-header]: http://www.sinatrarb.com/protection/xss_header
@@ -0,0 +1,72 @@
1
+ # encoding: utf-8
2
+ $LOAD_PATH.unshift File.expand_path('../lib', __FILE__)
3
+
4
+ begin
5
+ require 'bundler'
6
+ Bundler::GemHelper.install_tasks
7
+ rescue LoadError => e
8
+ $stderr.puts e
9
+ end
10
+
11
+ desc "run specs"
12
+ task(:spec) { ruby '-S rspec spec' }
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 :index do
28
+ doc = File.read("README.md")
29
+ file = "doc/rack-protection-readme.md"
30
+ Dir.mkdir "doc" unless File.directory? "doc"
31
+ puts "writing #{file}"
32
+ File.open(file, "w") { |f| f << doc }
33
+ end
34
+
35
+ task :all => [:readmes, :index]
36
+ end
37
+
38
+ desc "generate documentation"
39
+ task :doc => 'doc:all'
40
+
41
+ desc "generate gemspec"
42
+ task 'rack-protection.gemspec' do
43
+ require 'rack/protection/version'
44
+ content = File.binread 'rack-protection.gemspec'
45
+
46
+ # fetch data
47
+ fields = {
48
+ :authors => `git shortlog -sn`.force_encoding('utf-8').scan(/[^\d\s].*/),
49
+ :email => ["mail@zzak.io", "konstantin.haase@gmail.com"],
50
+ :files => %w(License README.md Rakefile Gemfile rack-protection.gemspec) + Dir['lib/**/*']
51
+ }
52
+
53
+ # insert data
54
+ fields.each do |field, values|
55
+ updated = " s.#{field} = ["
56
+ updated << values.map { |v| "\n %p" % v }.join(',')
57
+ updated << "\n ]"
58
+ content.sub!(/ s\.#{field} = \[\n( .*\n)* \]/, updated)
59
+ end
60
+
61
+ # set version
62
+ content.sub! /(s\.version.*=\s+).*/, "\\1\"#{Rack::Protection::VERSION}\""
63
+
64
+ # escape unicode
65
+ content.gsub!(/./) { |c| c.bytesize > 1 ? "\\u{#{c.codepoints.first.to_s(16)}}" : c }
66
+
67
+ File.open('rack-protection.gemspec', 'w') { |f| f << content }
68
+ end
69
+
70
+ task :gemspec => 'rack-protection.gemspec'
71
+ task :default => :spec
72
+ task :test => :spec
@@ -0,0 +1 @@
1
+ require "rack/protection"
@@ -0,0 +1,54 @@
1
+ require 'rack/protection/version'
2
+ require 'rack'
3
+
4
+ module Rack
5
+ module Protection
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'
22
+
23
+ def self.new(app, options = {})
24
+ # does not include: RemoteReferrer, AuthenticityToken and FormToken
25
+ except = Array options[:except]
26
+ use_these = Array options[:use]
27
+
28
+ if options.fetch(:without_session, false)
29
+ except += [:session_hijacking, :remote_token]
30
+ end
31
+
32
+ Rack::Builder.new do
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
50
+ run app
51
+ end.to_app
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,196 @@
1
+ require 'rack/protection'
2
+ require 'securerandom'
3
+ require 'base64'
4
+
5
+ module Rack
6
+ module Protection
7
+ ##
8
+ # Prevented attack:: CSRF
9
+ # Supported browsers:: all
10
+ # More infos:: http://en.wikipedia.org/wiki/Cross-site_request_forgery
11
+ #
12
+ # This middleware only accepts requests other than <tt>GET</tt>,
13
+ # <tt>HEAD</tt>, <tt>OPTIONS</tt>, <tt>TRACE</tt> if their given access
14
+ # token matches the token included in the session.
15
+ #
16
+ # It checks the <tt>X-CSRF-Token</tt> header and the <tt>POST</tt> form
17
+ # data.
18
+ #
19
+ # Compatible with the {rack-csrf}[https://rubygems.org/gems/rack_csrf] gem.
20
+ #
21
+ # == Options
22
+ #
23
+ # [<tt>:authenticity_param</tt>] the name of the param that should contain
24
+ # the token on a request. Default value:
25
+ # <tt>"authenticity_token"</tt>
26
+ #
27
+ # == Example: Forms application
28
+ #
29
+ # To show what the AuthenticityToken does, this section includes a sample
30
+ # program which shows two forms. One with, and one without a CSRF token
31
+ # The one without CSRF token field will get a 403 Forbidden response.
32
+ #
33
+ # Install the gem, then run the program:
34
+ #
35
+ # gem install 'rack-protection'
36
+ # ruby server.rb
37
+ #
38
+ # Here is <tt>server.rb</tt>:
39
+ #
40
+ # require 'rack/protection'
41
+ #
42
+ # app = Rack::Builder.app do
43
+ # use Rack::Session::Cookie, secret: 'secret'
44
+ # use Rack::Protection::AuthenticityToken
45
+ #
46
+ # run -> (env) do
47
+ # [200, {}, [
48
+ # <<~EOS
49
+ # <!DOCTYPE html>
50
+ # <html lang="en">
51
+ # <head>
52
+ # <meta charset="UTF-8" />
53
+ # <title>rack-protection minimal example</title>
54
+ # </head>
55
+ # <body>
56
+ # <h1>Without Authenticity Token</h1>
57
+ # <p>This takes you to <tt>Forbidden</tt></p>
58
+ # <form action="" method="post">
59
+ # <input type="text" name="foo" />
60
+ # <input type="submit" />
61
+ # </form>
62
+ #
63
+ # <h1>With Authenticity Token</h1>
64
+ # <p>This successfully takes you to back to this form.</p>
65
+ # <form action="" method="post">
66
+ # <input type="hidden" name="authenticity_token" value="#{env['rack.session'][:csrf]}" />
67
+ # <input type="text" name="foo" />
68
+ # <input type="submit" />
69
+ # </form>
70
+ # </body>
71
+ # </html>
72
+ # EOS
73
+ # ]]
74
+ # end
75
+ # end
76
+ #
77
+ # Rack::Handler::WEBrick.run app
78
+ #
79
+ # == Example: Customize which POST parameter holds the token
80
+ #
81
+ # To customize the authenticity parameter for form data, use the
82
+ # <tt>:authenticity_param</tt> option:
83
+ # use Rack::Protection::AuthenticityToken, authenticity_param: 'your_token_param_name'
84
+ class AuthenticityToken < Base
85
+ TOKEN_LENGTH = 32
86
+
87
+ default_options :authenticity_param => 'authenticity_token',
88
+ :allow_if => nil
89
+
90
+ def self.token(session)
91
+ self.new(nil).mask_authenticity_token(session)
92
+ end
93
+
94
+ def self.random_token
95
+ SecureRandom.base64(TOKEN_LENGTH)
96
+ end
97
+
98
+ def accepts?(env)
99
+ session = session env
100
+ set_token(session)
101
+
102
+ safe?(env) ||
103
+ valid_token?(session, env['HTTP_X_CSRF_TOKEN']) ||
104
+ valid_token?(session, Request.new(env).params[options[:authenticity_param]]) ||
105
+ ( options[:allow_if] && options[:allow_if].call(env) )
106
+ end
107
+
108
+ def mask_authenticity_token(session)
109
+ token = set_token(session)
110
+ mask_token(token)
111
+ end
112
+
113
+ private
114
+
115
+ def set_token(session)
116
+ session[:csrf] ||= self.class.random_token
117
+ end
118
+
119
+ # Checks the client's masked token to see if it matches the
120
+ # session token.
121
+ def valid_token?(session, token)
122
+ return false if token.nil? || token.empty?
123
+
124
+ begin
125
+ token = decode_token(token)
126
+ rescue ArgumentError # encoded_masked_token is invalid Base64
127
+ return false
128
+ end
129
+
130
+ # See if it's actually a masked token or not. We should be able
131
+ # to handle any unmasked tokens that we've issued without error.
132
+
133
+ if unmasked_token?(token)
134
+ compare_with_real_token token, session
135
+
136
+ elsif masked_token?(token)
137
+ token = unmask_token(token)
138
+
139
+ compare_with_real_token token, session
140
+
141
+ else
142
+ false # Token is malformed
143
+ end
144
+ end
145
+
146
+ # Creates a masked version of the authenticity token that varies
147
+ # on each request. The masking is used to mitigate SSL attacks
148
+ # like BREACH.
149
+ def mask_token(token)
150
+ token = decode_token(token)
151
+ one_time_pad = SecureRandom.random_bytes(token.length)
152
+ encrypted_token = xor_byte_strings(one_time_pad, token)
153
+ masked_token = one_time_pad + encrypted_token
154
+ encode_token(masked_token)
155
+ end
156
+
157
+ # Essentially the inverse of +mask_token+.
158
+ def unmask_token(masked_token)
159
+ # Split the token into the one-time pad and the encrypted
160
+ # value and decrypt it
161
+ token_length = masked_token.length / 2
162
+ one_time_pad = masked_token[0...token_length]
163
+ encrypted_token = masked_token[token_length..-1]
164
+ xor_byte_strings(one_time_pad, encrypted_token)
165
+ end
166
+
167
+ def unmasked_token?(token)
168
+ token.length == TOKEN_LENGTH
169
+ end
170
+
171
+ def masked_token?(token)
172
+ token.length == TOKEN_LENGTH * 2
173
+ end
174
+
175
+ def compare_with_real_token(token, session)
176
+ secure_compare(token, real_token(session))
177
+ end
178
+
179
+ def real_token(session)
180
+ decode_token(session[:csrf])
181
+ end
182
+
183
+ def encode_token(token)
184
+ Base64.strict_encode64(token)
185
+ end
186
+
187
+ def decode_token(token)
188
+ Base64.strict_decode64(token)
189
+ end
190
+
191
+ def xor_byte_strings(s1, s2)
192
+ s1.bytes.zip(s2.bytes).map { |(c1,c2)| c1 ^ c2 }.pack('c*')
193
+ end
194
+ end
195
+ end
196
+ end