roda 3.8.0 → 3.9.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f1f45dc2c17fe99add9644d7ee24bcc83e85f0c4ca5927b7d88b31ddac7c4d35
4
- data.tar.gz: 654f4aaa9987343d9ff989bc4fc730d2c07d885b4ee57465be845c0876636340
3
+ metadata.gz: 39ce926351959abb8a72a7b1c9db596dff7e0e84c26c296db4d2b3c124486356
4
+ data.tar.gz: 904ab94da8c67bfd2d4acb5246fa0a3049c89778e2fbc9e1701f6f342f39329a
5
5
  SHA512:
6
- metadata.gz: 9766752c2e4204821986db58bc086080f406770672670179a65bfe91766227f2637c195250d66d0f1b8ad021e6a3593b901a649cd51ab5484e7249cda057498c
7
- data.tar.gz: e9ad5ba69df715e11e3ee9b258c2e4233631104cbdf972c2c8e0c6ace1bbf1fa5b6bca5cb88d2891e63fb64885ba17623a35760e6b60f49c4e56cebf43a17c1a
6
+ metadata.gz: 590cc6b72c9fbb3fc01e389a6c12a5d09cd0db1cf56fe0d821b79e0e3aaf1d91ca7598d25fbdbdc50b25171f79501087599bd334ce079ad435ee51be31988588
7
+ data.tar.gz: 8bcde66bd4ab812a963ace04db8b026df5d5b5c9d79432de4b5780da80f9548f1dc92681e2fe0bdd1330d20f526cab4cea27b4bd0d54dddedbc8b5a9ec864c77
data/CHANGELOG CHANGED
@@ -1,3 +1,7 @@
1
+ = 3.9.0 (2018-06-11)
2
+
3
+ * Add route_csrf plugin for CSRF protection, offering more control, better security, and request-specific tokens compared to rack_csrf (jeremyevans)
4
+
1
5
  = 3.8.0 (2018-05-17)
2
6
 
3
7
  * Accept convert_each! :keys option that is Proc or Method in typecast_params plugin (jeremyevans)
data/README.rdoc CHANGED
@@ -788,12 +788,24 @@ This means you should not store any secret data in the session.
788
788
 
789
789
  === Cross Site Request Forgery (CSRF)
790
790
 
791
- CSRF can be prevented by using the +csrf+ plugin that ships with Roda,
792
- which uses the {rack_csrf}[https://github.com/baldowl/rack_csrf] library.
793
- Just make sure that you include the CSRF token tags in your HTML, as appropriate.
791
+ CSRF can be prevented by using the +route_csrf+ plugin that ships with Roda.
792
+ The +route_csrf+ plugin uses modern security practices to create CSRF tokens,
793
+ requires request-specific tokens by default, and offers control to the user
794
+ over where in the routing tree that CSRF tokens are checked. For example, if
795
+ you are using the +public+ plugin to serve static files and the +assets+
796
+ plugin to serve assets, you wouldn't need to check for CSRF tokens for either
797
+ of those, so you could put the CSRF check after those in the routing tree,
798
+ but before handling other requests:
799
+
800
+ route do |r|
801
+ r.public
802
+ r.assets
803
+
804
+ check_csrf! # Must call this to check for valid CSRF tokens
805
+
806
+ # ...
807
+ end
794
808
 
795
- It's also possible to use the <tt>Rack::Csrf</tt> middleware directly;
796
- you don't have to use the +csrf+ plugin.
797
809
 
798
810
  === Cross Site Scripting (XSS)
799
811
 
@@ -0,0 +1,67 @@
1
+ = New Features
2
+
3
+ * A route_csrf plugin has been added. This plugin allows for more
4
+ control over CSRF protection, since the user can choose where in
5
+ the routing tree to enforce the protection. Additionally, the
6
+ route_csrf plugin offers better security than the CSRF protection
7
+ used by the csrf plugin (which uses the rack_csrf library).
8
+
9
+ The route_csrf plugin defaults to allowing only CSRF tokens
10
+ specific to a given request method and request path, and not
11
+ allowing generic CSRF tokens (though it does offer optional support
12
+ for such tokens). Both request-specific and generic CSRF tokens
13
+ are designed to never leak the CSRF secret key, making it more
14
+ difficult to forge valid CSRF tokens. Additionally, the plugin
15
+ offers optional support for accepting rack_csrf tokens, which
16
+ should only be enabled during a short transition period.
17
+
18
+ Some differences between the route_csrf plugin and the older
19
+ csrf plugin:
20
+
21
+ * route_csrf supports and by default only allows CSRF tokens
22
+ specific to request method and request path, as mentioned
23
+ above. You can use the require_request_specific_tokens: false
24
+ option to allow generic CSRF tokens.
25
+
26
+ * route_csrf does not check the HTTP header by default, it
27
+ only checks the header if the :check_header option is set.
28
+ The :check_header option can be set to true to check both
29
+ the parameter and the header, or set to :only to only check
30
+ the header.
31
+
32
+ * route_csrf raises by default for invalid CSRF tokens. rack_csrf
33
+ returns an empty 403 response in that case. You can use the
34
+ error_handler plugin to handle the
35
+ Roda::RodaPlugins::RouteCsrf::InvalidToken exceptions, or you
36
+ can use the csrf_failure: :empty_403 option if you would like
37
+ the csrf plugin default behavior. The plugin also accepts a
38
+ block for configurable failure behavior.
39
+
40
+ * route_csrf does not use a middleware, as it is designed to give
41
+ more control. In order to enforce the CSRF protection, you need
42
+ to call check_csrf! in your routing tree at the appropriate
43
+ place. If you are not sure where to add it, add it to the top
44
+ of the routing tree, after the public or assets routes if you
45
+ are using those plugins:
46
+
47
+ route do
48
+ r.public
49
+ r.assets
50
+ check_csrf!
51
+
52
+ # ...
53
+ end
54
+
55
+ The check_csrf! method accepts an options hash, which can be used
56
+ to override the plugin options on a per-call basis.
57
+
58
+ * The csrf_token/csrf_tag methods take an optional path and method
59
+ arguments. If a path is given, the method defaults to POST, and
60
+ the resulting CSRF token can only be used to submit forms for the
61
+ path and method. If a path is not given, the resulting CSRF token
62
+ will be generic, but it will only work if the plugin has been
63
+ configured to allow generic CSRF tokens.
64
+
65
+ * A csrf_path method is available for easily taking a form action
66
+ string and returning an appropriate path to pass to the csrf_token
67
+ or csrf_tag methods.
@@ -4,6 +4,10 @@ require 'rack/csrf'
4
4
 
5
5
  class Roda
6
6
  module RodaPlugins
7
+ # This plugin is no longer recommended for use, it exists only for
8
+ # backwards compatibility. Consider using the route_csrf plugin
9
+ # instead, as that provides stronger CSRF protection.
10
+ #
7
11
  # The csrf plugin adds CSRF protection using rack_csrf, along with
8
12
  # some csrf helper methods to use in your views. To use it, load
9
13
  # the plugin, with the options hash passed to Rack::Csrf:
@@ -0,0 +1,351 @@
1
+ # frozen-string-literal: true
2
+
3
+ require 'base64'
4
+ require 'openssl'
5
+ require 'securerandom'
6
+ require 'uri'
7
+
8
+ class Roda
9
+ module RodaPlugins
10
+ # The route_csrf plugin is the recommended plugin to use to support
11
+ # CSRF protection in Roda applications. This plugin allows you set
12
+ # where in the routing tree to enforce CSRF protection. Additionally,
13
+ # the route_csrf plugin uses modern security practices.
14
+ #
15
+ # By default, the plugin requires tokens be specific to the request
16
+ # method and request path, so a CSRF token generated for one form will
17
+ # not be usable to submit a different form.
18
+ #
19
+ # This plugin also takes care to not expose the underlying CSRF key
20
+ # (except in the session), so that it is not possible for an attacker
21
+ # to generate valid CSRF tokens specific to an arbitrary request method
22
+ # and request path even if they have access to a token that is not
23
+ # specific to request method and request path. To get this security
24
+ # benefit, you must ensure an attacker does not have access to the
25
+ # session. Rack::Session::Cookie uses signed sessions, not encrypted
26
+ # sessions, so if the attacker has the ability to read cookie data
27
+ # and you are using Rack::Session::Cookie, it will still be possible
28
+ # for an attacker to generate valid CSRF tokens specific to arbitrary
29
+ # request method and request path.
30
+ #
31
+ # # Usage
32
+ #
33
+ # It is recommended to use the plugin defaults, loading the
34
+ # plugin with no options:
35
+ #
36
+ # plugin :route_csrf
37
+ #
38
+ # This plugin supports the following options:
39
+ #
40
+ # :field :: Form input parameter name for CSRF token (default: '_csrf')
41
+ # :header :: HTTP header name for CSRF token (default: 'X-CSRF-Token')
42
+ # :key :: Session key for CSRF secret (default: '_roda_csrf_secret')
43
+ # :require_request_specific_tokens :: Whether request-specific tokens are required (default: true).
44
+ # A false value will allow tokens that are not request-specific
45
+ # to also work. You should only set this to false if it is
46
+ # impossible to use request-specific tokens. If you must
47
+ # use non-request-specific tokens in certain cases, it is best
48
+ # to leave this option true by default, and override it on a
49
+ # per call basis in those specific cases.
50
+ # :csrf_failure :: The action to taken if a request fails the CSRF check (default: :raise). Options:
51
+ # :raise :: raise a Roda::RodaPlugins::RouteCsrf::InvalidToken exception
52
+ # :empty_403 :: return a blank 403 page (rack_csrf's default behavior)
53
+ # :clear_session :: Clear the current session
54
+ # Proc :: Treated as a routing block, called with request object
55
+ # :check_header :: Whether the HTTP header should be checked for the token value (default: false).
56
+ # If true, checks the HTTP header after checking for the form input parameter.
57
+ # If :only, only checks the HTTP header and doesn't check the form input parameter.
58
+ # :check_request_methods :: Which request methods require CSRF protection
59
+ # (default: <tt>['POST', 'DELETE', 'PATCH', 'PUT']</tt>)
60
+ # :upgrade_from_rack_csrf_key :: If provided, the session key that should be checked for the
61
+ # rack_csrf raw token. If the session key is present, the value
62
+ # will be checked against the submitted token, and if it matches,
63
+ # the CSRF check will be passed. Should only be set temporarily
64
+ # if upgrading from using rack_csrf to the route_csrf plugin, and
65
+ # should be removed as soon as you are OK with CSRF forms generated
66
+ # before the upgrade not longer being usable. The default rack_csrf
67
+ # key is <tt>'csrf.token'</tt>.
68
+ #
69
+ # The plugin also supports a block, in which case the block will be used
70
+ # as the value of the :csrf_failure option.
71
+ #
72
+ # # Methods
73
+ #
74
+ # This adds the following instance methods:
75
+ #
76
+ # check_csrf!(opts={}) :: Used for checking if the submitted CSRF token is valid.
77
+ # If a block is provided, it is treated as a routing block if the
78
+ # CSRF token is not valid. Otherwise, by default, raises a
79
+ # Roda::RodaPlugins::RouteCsrf::InvalidToken exception if a CSRF
80
+ # token is necessary for the request and there is no token provided
81
+ # or the provided token is not valid. Options can be provided to
82
+ # override any of the plugin options for this specific call.
83
+ # The :token option can be used to specify the provided CSRF token
84
+ # (instead of looking for the token in the submitted parameters).
85
+ # csrf_field :: The field name to use for the hidden tag containing the CSRF token.
86
+ # csrf_path(action) :: This takes an argument that would be the value of the HTML form's
87
+ # action attribute, and returns a path you can pass to csrf_token
88
+ # that should be valid for the form submission. The argument should
89
+ # either be nil or a string representing a relative path, absolute
90
+ # path, or full URL.
91
+ # csrf_tag(path=nil, method='POST') :: An HTML hidden input tag string containing the CSRF token, suitable
92
+ # for placing in an HTML form. Takes the same arguments as csrf_token.
93
+ # csrf_token(path=nil, method='POST') :: The value of the csrf token, in case it needs to be accessed
94
+ # directly. It is recommended to call this method with a
95
+ # path, which will create a request-specific token. Calling
96
+ # this method without an argument will create a token that is
97
+ # not specific to the request, but such a token will only
98
+ # work if you set the :require_request_specific_tokens option
99
+ # to false, which is a bad idea from a security standpoint.
100
+ # use_request_specific_csrf_tokens? :: Whether the plugin is configured to only support
101
+ # request-specific tokens, true by default.
102
+ # valid_csrf?(opts={}) :: Returns whether the submitted CSRF token is valid (also true if
103
+ # the request does not require a CSRF token). Takes same option hash
104
+ # as check_csrf!.
105
+ #
106
+ # This plugin also adds the following instance methods for compatibility with the
107
+ # older csrf plugin, but it is not recommended to use these methods in new code:
108
+ #
109
+ # csrf_header :: The header name to use for submitting the CSRF token via an HTTP header
110
+ # (useful for javascript). Note that this plugin will not look in
111
+ # the HTTP header by default, it will only do so if the :check_header
112
+ # option is used.
113
+ # csrf_metatag :: An HTML meta tag string containing the CSRF token, suitable
114
+ # for placing in the page header. It is not recommended to use
115
+ # this method, as the token generated is not request-specific and
116
+ # will not work unless you set the :require_request_specific_tokens option to
117
+ # false, which is a bad idea from a security standpoint.
118
+ #
119
+ # # Token Cryptography
120
+ #
121
+ # route_csrf uses HMAC-SHA-256 to generate all CSRF tokens. It generates a random 32-byte secret,
122
+ # which is stored base64 encoded in the session. For each CSRF token, it generates 31 bytes
123
+ # of random data.
124
+ #
125
+ # For request-specific CSRF tokens, this pseudocode generates the HMAC:
126
+ #
127
+ # hmac = HMAC(secret, method + path + random_data)
128
+ #
129
+ # For CSRF tokens not specific to a request, this pseudocode generates the HMAC:
130
+ #
131
+ # hmac = HMAC(secret, random_data)
132
+ #
133
+ # This pseudocode generates the final CSRF token in both cases:
134
+ #
135
+ # token = Base64Encode(random_data + hmac)
136
+ #
137
+ # Using this construction for generating CSRF tokens means that generating any
138
+ # valid CSRF token without knowledge of the secret is equivalent to a successful generic attack
139
+ # on HMAC-SHA-256.
140
+ #
141
+ # By using an HMAC for tokens not specific to a request, it is not possible to use a
142
+ # valid CSRF token that is not specific to a request to generate a valid request-specific
143
+ # CSRF token.
144
+ #
145
+ # By including random data in the HMAC for all tokens, different tokens are generated
146
+ # each time, mitigating compression ratio attacks such as BREACH.
147
+ module RouteCsrf
148
+ # Default CSRF option values
149
+ DEFAULTS = {
150
+ :field => '_csrf'.freeze,
151
+ :header => 'X-CSRF-Token'.freeze,
152
+ :key => '_roda_csrf_secret'.freeze,
153
+ :require_request_specific_tokens => true,
154
+ :csrf_failure => :raise,
155
+ :check_header => false,
156
+ :check_request_methods => %w'POST DELETE PATCH PUT'.freeze.each(&:freeze)
157
+ }.freeze
158
+
159
+ # Exception class raised when :csrf_failure option is :raise and
160
+ # a valid CSRF token was not provided.
161
+ class InvalidToken < RodaError; end
162
+
163
+ def self.configure(app, opts=OPTS, &block)
164
+ options = app.opts[:route_csrf] = (app.opts[:route_csrf] || DEFAULTS).merge(opts)
165
+ if block
166
+ if opts[:csrf_failure]
167
+ raise RodaError, "Cannot specify both route_csrf plugin block and :csrf_failure option"
168
+ end
169
+ options[:csrf_failure] = block
170
+ end
171
+ options[:env_header] = "HTTP_#{options[:header].to_s.gsub('-', '_').upcase}".freeze
172
+ options.freeze
173
+ end
174
+
175
+ module InstanceMethods
176
+ # Check that the submitted CSRF token is valid, if the request requires a CSRF token.
177
+ # If the CSRF token is valid or the request does not require a CSRF token, return nil.
178
+ # Otherwise, if a block is given, treat it as a routing block and yield to it, and
179
+ # if a block is not given, use the :csrf_failure option to determine how to handle it.
180
+ def check_csrf!(opts=OPTS, &block)
181
+ if msg = csrf_invalid_message(opts)
182
+ if block
183
+ @_request.on(&block)
184
+ end
185
+
186
+ case failure_action = opts.fetch(:csrf_failure, csrf_options[:csrf_failure])
187
+ when :raise
188
+ raise InvalidToken, msg
189
+ when :empty_403
190
+ throw :halt, [403, {'Content-Type'=>'text/html', 'Content-Length'=>'0'}, []]
191
+ when :clear_session
192
+ session.clear
193
+ when Proc
194
+ @_request.on{instance_exec(@_request, &failure_action)}
195
+ else
196
+ raise RodaError, "Unsupported :csrf_failure option: #{failure_action.inspect}"
197
+ end
198
+ end
199
+ end
200
+
201
+ # The name of the hidden input tag containing the CSRF token. Also used as the name
202
+ # for the meta tag.
203
+ def csrf_field
204
+ csrf_options[:field]
205
+ end
206
+
207
+ # The HTTP header name to use when submitting CSRF tokens in an HTTP header, if
208
+ # such support is enabled (it is not by default).
209
+ def csrf_header
210
+ csrf_options[:header]
211
+ end
212
+
213
+ # An HTML meta tag string containing a CSRF token that is not request-specific.
214
+ # It is not recommended to use this, as it doesn't support request-specific tokens.
215
+ def csrf_metatag
216
+ "<meta name=\"#{csrf_field}\" content=\"#{csrf_token}\" \/>"
217
+ end
218
+
219
+ # Given a form action, return the appropriate path to use for the CSRF token.
220
+ # This makes it easier to generate request-specific tokens without having to
221
+ # worry about the different types of form actions (relative paths, absolute
222
+ # paths, URLs, empty paths).
223
+ def csrf_path(action)
224
+ case action
225
+ when nil, '', /\A[#?]/
226
+ # use current path
227
+ request.path
228
+ when /\A(?:https?:\/)?\//
229
+ # Either full URI or absolute path, extract just the path
230
+ URI.parse(action).path
231
+ else
232
+ # relative path, join to current path
233
+ URI.join(request.url, action).path
234
+ end
235
+ end
236
+
237
+ # An HTML hidden input tag string containing the CSRF token. See csrf_token for
238
+ # arguments.
239
+ def csrf_tag(*args)
240
+ "<input type=\"hidden\" name=\"#{csrf_field}\" value=\"#{csrf_token(*args)}\" \/>"
241
+ end
242
+
243
+ # The value of the csrf token. For a path specific token, provide a path
244
+ # argument. By default, it a path is provided, the POST request method will
245
+ # be assumed. To generate a token for a non-POST request method, pass the
246
+ # method as the second argument.
247
+ def csrf_token(path=nil, method=('POST' if path))
248
+ token = SecureRandom.random_bytes(31)
249
+ token << csrf_hmac(token, method, path)
250
+ Base64.strict_encode64(token)
251
+ end
252
+
253
+ # Whether request-specific CSRF tokens should be used by default.
254
+ def use_request_specific_csrf_tokens?
255
+ csrf_options[:require_request_specific_tokens]
256
+ end
257
+
258
+ # Whether the submitted CSRF token is valid for the request. True if the
259
+ # request does not require a CSRF token.
260
+ def valid_csrf?(opts=OPTS)
261
+ csrf_invalid_message(opts).nil?
262
+ end
263
+
264
+ private
265
+
266
+ # Returns error message string if the CSRF token is not valid.
267
+ # Returns nil if the CSRF token is valid.
268
+ def csrf_invalid_message(opts)
269
+ opts = opts.empty? ? csrf_options : csrf_options.merge(opts)
270
+ method = request.request_method
271
+
272
+ unless opts[:check_request_methods].include?(method)
273
+ return
274
+ end
275
+
276
+ unless encoded_token = opts[:token]
277
+ encoded_token = case opts[:check_header]
278
+ when :only
279
+ env[opts[:env_header]]
280
+ when true
281
+ return (csrf_invalid_message(opts.merge(:check_header=>false)) && csrf_invalid_message(opts.merge(:check_header=>:only)))
282
+ else
283
+ @_request.params[opts[:field]]
284
+ end
285
+ end
286
+
287
+ unless encoded_token.is_a?(String)
288
+ return "encoded token is not a string"
289
+ end
290
+
291
+ if (rack_csrf_key = opts[:upgrade_from_rack_csrf_key]) && (rack_csrf_value = session[rack_csrf_key]) && csrf_compare(rack_csrf_value, encoded_token)
292
+ return
293
+ end
294
+
295
+ # 31 byte random initialization vector
296
+ # 32 byte HMAC
297
+ # 63 bytes total
298
+ # 84 bytes when base64 encoded
299
+ unless encoded_token.bytesize == 84
300
+ return "encoded token length is not 84"
301
+ end
302
+
303
+ begin
304
+ submitted_hmac = Base64.strict_decode64(encoded_token)
305
+ rescue ArgumentError
306
+ return "encoded token is not valid base64"
307
+ end
308
+
309
+ random_data = submitted_hmac.slice!(0...31)
310
+
311
+ if csrf_compare(csrf_hmac(random_data, method, @_request.path), submitted_hmac)
312
+ return
313
+ end
314
+
315
+ if opts[:require_request_specific_tokens]
316
+ "decoded token is not valid for request method and path"
317
+ else
318
+ unless csrf_compare(csrf_hmac(random_data, '', ''), submitted_hmac)
319
+ "decoded token is not valid for either request method and path or for blank method and path"
320
+ end
321
+ end
322
+ end
323
+
324
+ # Helper for getting the plugin options.
325
+ def csrf_options
326
+ opts[:route_csrf]
327
+ end
328
+
329
+ # Perform a constant-time comparison of the two strings, returning true if they match and false otherwise.
330
+ def csrf_compare(s1, s2)
331
+ Rack::Utils.secure_compare(s1, s2)
332
+ end
333
+
334
+ # Return the HMAC-SHA-256 for the secret and the given arguments.
335
+ def csrf_hmac(random_data, method, path)
336
+ OpenSSL::HMAC.digest(OpenSSL::Digest::SHA256.new, csrf_secret, "#{method.to_s.upcase}#{path}#{random_data}")
337
+ end
338
+
339
+ # If a secret has not already been specified, generate a random 32-byte
340
+ # secret, stored base64 encoded in the session (to handle cases where
341
+ # JSON is used for session serialization).
342
+ def csrf_secret
343
+ key = session[csrf_options[:key]] ||= SecureRandom.base64(32)
344
+ Base64.strict_decode64(key)
345
+ end
346
+ end
347
+ end
348
+
349
+ register_plugin(:route_csrf, RouteCsrf)
350
+ end
351
+ end
@@ -665,8 +665,8 @@ class Roda
665
665
  v = array(type, key, default)
666
666
 
667
667
  if key.is_a?(Array)
668
- key.zip(v).each do |key, arr|
669
- check_array!(key, arr)
668
+ key.zip(v).each do |k, arr|
669
+ check_array!(k, arr)
670
670
  end
671
671
  else
672
672
  check_array!(key, v)
data/lib/roda/version.rb CHANGED
@@ -4,7 +4,7 @@ class Roda
4
4
  RodaMajorVersion = 3
5
5
 
6
6
  # The minor version of Roda, updated for new feature releases of Roda.
7
- RodaMinorVersion = 8
7
+ RodaMinorVersion = 9
8
8
 
9
9
  # The patch version of Roda, updated only for bug fixes from the last
10
10
  # feature release.
@@ -6,6 +6,8 @@ rescue LoadError
6
6
  warn "rack_csrf not installed, skipping csrf plugin test"
7
7
  else
8
8
  describe "csrf plugin" do
9
+ include CookieJar
10
+
9
11
  it "adds csrf protection and csrf helper methods" do
10
12
  app(:bare) do
11
13
  use Rack::Session::Cookie, :secret=>'1'
@@ -33,7 +35,6 @@ describe "csrf plugin" do
33
35
  status('REQUEST_METHOD'=>'POST', 'rack.input'=>io).must_equal 403
34
36
  body('/foo', 'REQUEST_METHOD'=>'POST', 'rack.input'=>io).must_equal 'bar'
35
37
 
36
- env = proc{|h| h['Set-Cookie'] ? {'HTTP_COOKIE'=>h['Set-Cookie'].sub("; path=/; HttpOnly", '')} : {}}
37
38
  s, h, b = req
38
39
  s.must_equal 200
39
40
  field = h['FIELD']
@@ -41,7 +42,7 @@ describe "csrf plugin" do
41
42
  h['TAG'].must_match(/\A<input type="hidden" name="#{field}" value="#{token}" \/>\z/)
42
43
  h['METATAG'].must_match(/\A<meta name="#{field}" content="#{token}" \/>\z/)
43
44
  b.must_equal ['g']
44
- s, _, b = req('/', env[h].merge('REQUEST_METHOD'=>'POST', 'rack.input'=>io, "HTTP_#{h['HEADER']}"=>h['TOKEN']))
45
+ s, _, b = req('REQUEST_METHOD'=>'POST', 'rack.input'=>io, "HTTP_#{h['HEADER']}"=>h['TOKEN'])
45
46
  s.must_equal 200
46
47
  b.must_equal ['p']
47
48
 
@@ -87,7 +88,6 @@ describe "csrf plugin" do
87
88
  status('/foo', 'REQUEST_METHOD'=>'POST', 'rack.input'=>io).must_equal 403
88
89
  body('/foo/bar', 'REQUEST_METHOD'=>'POST', 'rack.input'=>io).must_equal 'foobar'
89
90
 
90
- env = proc{|h| h['Set-Cookie'] ? {'HTTP_COOKIE'=>h['Set-Cookie'].sub("; path=/; HttpOnly", '')} : {}}
91
91
  s, h, b = req('/foo')
92
92
  s.must_equal 200
93
93
  field = h['FIELD']
@@ -95,7 +95,7 @@ describe "csrf plugin" do
95
95
  h['TAG'].must_match(/\A<input type="hidden" name="#{field}" value="#{token}" \/>\z/)
96
96
  h['METATAG'].must_match(/\A<meta name="#{field}" content="#{token}" \/>\z/)
97
97
  b.must_equal ['g']
98
- s, _, b = req('/foo', env[h].merge('REQUEST_METHOD'=>'POST', 'rack.input'=>io, "HTTP_#{h['HEADER']}"=>h['TOKEN']))
98
+ s, _, b = req('/foo', 'REQUEST_METHOD'=>'POST', 'rack.input'=>io, "HTTP_#{h['HEADER']}"=>h['TOKEN'])
99
99
  s.must_equal 200
100
100
  b.must_equal ['p']
101
101
 
@@ -1,6 +1,8 @@
1
1
  require_relative "../spec_helper"
2
2
 
3
3
  describe "flash plugin" do
4
+ include CookieJar
5
+
4
6
  it "flash.now[] sets flash for current page" do
5
7
  app(:bare) do
6
8
  use Rack::Session::Cookie, :secret => "1"
@@ -34,20 +36,19 @@ describe "flash plugin" do
34
36
  end
35
37
  end
36
38
 
37
- env = proc{|h| h['Set-Cookie'] ? {'HTTP_COOKIE'=>h['Set-Cookie'].sub("; path=/; HttpOnly", '')} : {}}
38
39
  _, h, b = req
39
40
  b.join.must_equal ''
40
- _, h, b = req(env[h])
41
+ _, h, b = req
41
42
  b.join.must_equal 'b'
42
- _, h, b = req(env[h])
43
+ _, h, b = req
43
44
  b.join.must_equal 'bb'
44
- _, h, b = req('/a', env[h])
45
+ _, h, b = req('/a')
45
46
  b.join.must_equal 'cbbb'
46
- _, h, b = req(env[h])
47
+ _, h, b = req
47
48
  b.join.must_equal ''
48
- _, h, b = req(env[h])
49
+ _, h, b = req
49
50
  b.join.must_equal 'b'
50
- _, h, b = req(env[h])
51
+ _, h, b = req
51
52
  b.join.must_equal 'bb'
52
53
  end
53
54
  end
@@ -40,7 +40,7 @@ describe "head plugin" do
40
40
  r.halt [ 200, {}, body ]
41
41
  end
42
42
  end
43
- s, h, b = req('REQUEST_METHOD' => 'HEAD')
43
+ s, _, b = req('REQUEST_METHOD' => 'HEAD')
44
44
  s.must_equal 200
45
45
  res = String.new
46
46
  body.closed?.must_equal false
@@ -0,0 +1,277 @@
1
+ require_relative "../spec_helper"
2
+
3
+ describe "route_csrf plugin" do
4
+ include CookieJar
5
+
6
+ def route_csrf_app(opts={}, &block)
7
+ app(:bare) do
8
+ use Rack::Session::Cookie, :secret=>'1'
9
+ plugin(:route_csrf, opts, &opts[:block])
10
+ route do |r|
11
+ check_csrf! unless env['SKIP']
12
+ r.post('foo'){'f'}
13
+ r.post('bar'){'b'}
14
+ r.get "token", String do |s|
15
+ csrf_token("/#{s}")
16
+ end
17
+ instance_exec(r, &block) if block
18
+ end
19
+ end
20
+ end
21
+
22
+ it "allows all GET requests and allows POST requests only if they have a correct token for the path" do
23
+ route_csrf_app
24
+ token = body("/token/foo")
25
+ token.length.must_equal 84
26
+ body("/foo", "REQUEST_METHOD"=>'POST', 'rack.input'=>StringIO.new("_csrf=#{Rack::Utils.escape(token)}")).must_equal 'f'
27
+ proc{body("/bar", "REQUEST_METHOD"=>'POST', 'rack.input'=>StringIO.new("_csrf=#{Rack::Utils.escape(token)}"))}.must_raise Roda::RodaPlugins::RouteCsrf::InvalidToken
28
+ proc{body("/foo", "REQUEST_METHOD"=>'PUT', 'rack.input'=>StringIO.new("_csrf=#{Rack::Utils.escape(token)}"))}.must_raise Roda::RodaPlugins::RouteCsrf::InvalidToken
29
+
30
+ token = body("/token/bar")
31
+ token.length.must_equal 84
32
+ body("/bar", "REQUEST_METHOD"=>'POST', 'rack.input'=>StringIO.new("_csrf=#{Rack::Utils.escape(token)}")).must_equal 'b'
33
+ proc{body("/foo", "REQUEST_METHOD"=>'POST', 'rack.input'=>StringIO.new("_csrf=#{Rack::Utils.escape(token)}"))}.must_raise Roda::RodaPlugins::RouteCsrf::InvalidToken
34
+ proc{body("/bar", "REQUEST_METHOD"=>'DELETE', 'rack.input'=>StringIO.new("_csrf=#{Rack::Utils.escape(token)}"))}.must_raise Roda::RodaPlugins::RouteCsrf::InvalidToken
35
+
36
+ # Additional failure cases
37
+
38
+ proc{body("/bar", "REQUEST_METHOD"=>'POST', 'rack.input'=>StringIO.new)}.must_raise Roda::RodaPlugins::RouteCsrf::InvalidToken
39
+
40
+ proc{body("/bar", "REQUEST_METHOD"=>'POST', 'rack.input'=>StringIO.new("_csrf=#{Rack::Utils.escape(token)}a"))}.must_raise Roda::RodaPlugins::RouteCsrf::InvalidToken
41
+
42
+ t2 = token.dup
43
+ t2.setbyte(1, t2.getbyte(1) ^ 1)
44
+ proc{body("/bar", "REQUEST_METHOD"=>'POST', 'rack.input'=>StringIO.new("_csrf=#{Rack::Utils.escape(t2)}"))}.must_raise Roda::RodaPlugins::RouteCsrf::InvalidToken
45
+
46
+ t2 = token.dup
47
+ t2.setbyte(61, t2.getbyte(61) ^ 1)
48
+ proc{body("/bar", "REQUEST_METHOD"=>'POST', 'rack.input'=>StringIO.new("_csrf=#{Rack::Utils.escape(t2)}"))}.must_raise Roda::RodaPlugins::RouteCsrf::InvalidToken
49
+
50
+ t2 = token.dup
51
+ t2[1] = '|'
52
+ proc{body("/bar", "REQUEST_METHOD"=>'POST', 'rack.input'=>StringIO.new("_csrf=#{Rack::Utils.escape(t2)}"))}.must_raise Roda::RodaPlugins::RouteCsrf::InvalidToken
53
+ end
54
+
55
+ it "supports :require_request_specific_tokens => false option to allow non-request-specific tokens" do
56
+ route_csrf_app(:require_request_specific_tokens=>false){csrf_token}
57
+ token = body("/token/foo")
58
+ token.length.must_equal 84
59
+ body("/foo", "REQUEST_METHOD"=>'POST', 'rack.input'=>StringIO.new("_csrf=#{Rack::Utils.escape(token)}")).must_equal 'f'
60
+ proc{body("/bar", "REQUEST_METHOD"=>'POST', 'rack.input'=>StringIO.new("_csrf=#{Rack::Utils.escape(token)}"))}.must_raise Roda::RodaPlugins::RouteCsrf::InvalidToken
61
+
62
+ token = body
63
+ token.length.must_equal 84
64
+ body("/foo", "REQUEST_METHOD"=>'POST', 'rack.input'=>StringIO.new("_csrf=#{Rack::Utils.escape(token)}")).must_equal 'f'
65
+ body("/bar", "REQUEST_METHOD"=>'POST', 'rack.input'=>StringIO.new("_csrf=#{Rack::Utils.escape(token)}")).must_equal 'b'
66
+ end
67
+
68
+ it "allows tokens submitted in both parameter and HTTP header if :check_header option is true" do
69
+ route_csrf_app(:check_header=>true)
70
+ token = body("/token/foo")
71
+ token.length.must_equal 84
72
+ body("/foo", "REQUEST_METHOD"=>'POST', 'rack.input'=>StringIO.new("_csrf=#{Rack::Utils.escape(token)}")).must_equal 'f'
73
+ body("/foo", "REQUEST_METHOD"=>'POST', 'rack.input'=>StringIO.new, 'HTTP_X_CSRF_TOKEN'=>token).must_equal 'f'
74
+ proc{body("/bar", "REQUEST_METHOD"=>'POST', 'rack.input'=>StringIO.new("_csrf=#{Rack::Utils.escape(token)}"))}.must_raise Roda::RodaPlugins::RouteCsrf::InvalidToken
75
+ proc{body("/foo", "REQUEST_METHOD"=>'PUT', 'rack.input'=>StringIO.new, 'HTTP_X_CSRF_TOKEN'=>token)}.must_raise Roda::RodaPlugins::RouteCsrf::InvalidToken
76
+ end
77
+
78
+ it "allows tokens submitted in only HTTP header if :check_header option is :only" do
79
+ route_csrf_app(:check_header=>:only)
80
+ token = body("/token/foo")
81
+ token.length.must_equal 84
82
+ body("/foo", "REQUEST_METHOD"=>'POST', 'rack.input'=>StringIO.new, 'HTTP_X_CSRF_TOKEN'=>token).must_equal 'f'
83
+ proc{body("/foo", "REQUEST_METHOD"=>'POST', 'rack.input'=>StringIO.new("_csrf=#{Rack::Utils.escape(token)}"))}.must_raise Roda::RodaPlugins::RouteCsrf::InvalidToken
84
+ proc{body("/bar", "REQUEST_METHOD"=>'POST', 'rack.input'=>StringIO.new("_csrf=#{Rack::Utils.escape(token)}"))}.must_raise Roda::RodaPlugins::RouteCsrf::InvalidToken
85
+ proc{body("/foo", "REQUEST_METHOD"=>'PUT', 'rack.input'=>StringIO.new, 'HTTP_X_CSRF_TOKEN'=>token)}.must_raise Roda::RodaPlugins::RouteCsrf::InvalidToken
86
+ end
87
+
88
+ it "allows configuring CSRF failure action with :csrf_failure => :empty_403 option" do
89
+ route_csrf_app(:csrf_failure=>:empty_403)
90
+ body("/foo", "REQUEST_METHOD"=>'POST', 'rack.input'=>StringIO.new("_csrf=#{Rack::Utils.escape(body("/token/foo"))}")).must_equal 'f'
91
+ req("/foo", "REQUEST_METHOD"=>'POST', 'rack.input'=>StringIO.new).must_equal [403, {'Content-Type'=>'text/html', 'Content-Length'=>'0'}, []]
92
+ end
93
+
94
+ it "allows configuring CSRF failure action with :csrf_failure => :empty_403 option" do
95
+ route_csrf_app(:csrf_failure=>:clear_session){session.inspect}
96
+ body("/foo", "REQUEST_METHOD"=>'POST', 'rack.input'=>StringIO.new("_csrf=#{Rack::Utils.escape(body("/token/foo"))}")).must_equal 'f'
97
+ body("/b", "REQUEST_METHOD"=>'POST', 'rack.input'=>StringIO.new("_csrf=#{Rack::Utils.escape(body('/token/a'))}")).must_equal '{}'
98
+ end
99
+
100
+ it "allows configuring CSRF failure action with :csrf_failure => proc option" do
101
+ route_csrf_app(:csrf_failure=>proc{|r| r.path + '2'})
102
+ body("/foo", "REQUEST_METHOD"=>'POST', 'rack.input'=>StringIO.new("_csrf=#{Rack::Utils.escape(body("/token/foo"))}")).must_equal 'f'
103
+ body("/foo", "REQUEST_METHOD"=>'POST', 'rack.input'=>StringIO.new).must_equal '/foo2'
104
+ end
105
+
106
+ it "allows configuring CSRF failure action via a plugin block" do
107
+ route_csrf_app(:block=>proc{|r| r.path + '2'})
108
+ body("/foo", "REQUEST_METHOD"=>'POST', 'rack.input'=>StringIO.new("_csrf=#{Rack::Utils.escape(body("/token/foo"))}")).must_equal 'f'
109
+ body("/foo", "REQUEST_METHOD"=>'POST', 'rack.input'=>StringIO.new).must_equal '/foo2'
110
+ end
111
+
112
+ it "raises Error if configuring plugin with invalid :csrf_failure option" do
113
+ route_csrf_app(:csrf_failure=>:foo)
114
+ proc{body("/foo", "REQUEST_METHOD"=>'POST', 'rack.input'=>StringIO.new)}.must_raise Roda::RodaError
115
+ end
116
+
117
+ it "raises Error if configuring plugin with block and :csrf_failure option" do
118
+ proc{route_csrf_app(:block=>proc{|r| r.path + '2'}, :csrf_failure=>:raise)}.must_raise Roda::RodaError
119
+ end
120
+
121
+ it "supports valid_csrf? method" do
122
+ route_csrf_app{valid_csrf?.to_s}
123
+ body("/a", "REQUEST_METHOD"=>'POST', 'rack.input'=>StringIO.new("_csrf=#{Rack::Utils.escape(body("/token/a"))}")).must_equal 'true'
124
+ body("/a", "REQUEST_METHOD"=>'POST', 'SKIP'=>true, 'rack.input'=>StringIO.new("_csrf=#{Rack::Utils.escape(body("/token/b"))}")).must_equal 'false'
125
+ end
126
+
127
+ it "supports valid_csrf? method" do
128
+ route_csrf_app do
129
+ check_csrf!{'nope'}
130
+ 'yep'
131
+ end
132
+ body("/a", "REQUEST_METHOD"=>'POST', 'SKIP'=>true, 'rack.input'=>StringIO.new("_csrf=#{Rack::Utils.escape(body("/token/a"))}")).must_equal 'yep'
133
+ body("/a", "REQUEST_METHOD"=>'POST', 'SKIP'=>true, 'rack.input'=>StringIO.new("_csrf=#{Rack::Utils.escape(body("/token/b"))}")).must_equal 'nope'
134
+ end
135
+
136
+ it "supports use_request_specific_csrf_tokens? method" do
137
+ route_csrf_app{use_request_specific_csrf_tokens?.to_s}
138
+ body.must_equal 'true'
139
+ route_csrf_app(:require_request_specific_tokens=>false){use_request_specific_csrf_tokens?.to_s}
140
+ body.must_equal 'false'
141
+ end
142
+
143
+ it "supports csrf_field method" do
144
+ route_csrf_app{csrf_field}
145
+ body.must_equal '_csrf'
146
+ route_csrf_app(:field=>'foo'){csrf_field}
147
+ body.must_equal 'foo'
148
+ end
149
+
150
+ it "supports csrf_header method" do
151
+ route_csrf_app{csrf_header}
152
+ body.must_equal 'X-CSRF-Token'
153
+ route_csrf_app(:header=>'Foo'){csrf_header}
154
+ body.must_equal 'Foo'
155
+ end
156
+
157
+ it "supports csrf_metatag method" do
158
+ route_csrf_app(:require_request_specific_tokens=>false){csrf_metatag}
159
+ body =~ /\A<meta name="_csrf" content="([+\/0-9A-Za-z]{84})" \/>\z/
160
+ body("/foo", "REQUEST_METHOD"=>'POST', 'rack.input'=>StringIO.new("_csrf=#{Rack::Utils.escape($1)}")).must_equal 'f'
161
+
162
+ route_csrf_app(:require_request_specific_tokens=>false, :field=>'foo'){csrf_metatag}
163
+ body =~ /\A<meta name="foo" content="([+\/0-9A-Za-z]{84})" \/>\z/
164
+ body("/bar", "REQUEST_METHOD"=>'POST', 'rack.input'=>StringIO.new("foo=#{Rack::Utils.escape($1)}")).must_equal 'b'
165
+ end
166
+
167
+ it "supports csrf_tag method" do
168
+ route_csrf_app(:require_request_specific_tokens=>false){csrf_tag}
169
+ body =~ /\A<input type="hidden" name="_csrf" value="([+\/0-9A-Za-z]{84})" \/>\z/
170
+ body("/foo", "REQUEST_METHOD"=>'POST', 'rack.input'=>StringIO.new("_csrf=#{Rack::Utils.escape($1)}")).must_equal 'f'
171
+
172
+ route_csrf_app(:require_request_specific_tokens=>false, :field=>'foo'){csrf_tag}
173
+ body =~ /\A<input type="hidden" name="foo" value="([+\/0-9A-Za-z]{84})" \/>\z/
174
+ body("/foo", "REQUEST_METHOD"=>'POST', 'rack.input'=>StringIO.new("foo=#{Rack::Utils.escape($1)}")).must_equal 'f'
175
+
176
+ route_csrf_app{csrf_tag('/foo')}
177
+ body =~ /\A<input type="hidden" name="_csrf" value="([+\/0-9A-Za-z]{84})" \/>\z/
178
+ token = $1
179
+ body("/foo", "REQUEST_METHOD"=>'POST', 'rack.input'=>StringIO.new("_csrf=#{Rack::Utils.escape(token)}")).must_equal 'f'
180
+ proc{body("/bar", "REQUEST_METHOD"=>'POST', 'rack.input'=>StringIO.new("_csrf=#{Rack::Utils.escape(token)}"))}.must_raise Roda::RodaPlugins::RouteCsrf::InvalidToken
181
+ proc{body("/foo", "REQUEST_METHOD"=>'PUT', 'rack.input'=>StringIO.new, 'QUERY_STRING'=>"_csrf=#{Rack::Utils.escape(token)}")}.must_raise Roda::RodaPlugins::RouteCsrf::InvalidToken
182
+
183
+ route_csrf_app do |r|
184
+ r.is 'foo', :method=>'PUT' do
185
+ 'f2'
186
+ end
187
+ csrf_tag('/foo', 'PUT')
188
+ end
189
+ body =~ /\A<input type="hidden" name="_csrf" value="([+\/0-9A-Za-z]{84})" \/>\z/
190
+ token = $1
191
+ body("/foo", "REQUEST_METHOD"=>'PUT', 'rack.input'=>StringIO.new, 'QUERY_STRING'=>"_csrf=#{Rack::Utils.escape(token)}").must_equal 'f2'
192
+ proc{body("/bar", "REQUEST_METHOD"=>'PUT', 'rack.input'=>StringIO.new, 'QUERY_STRING'=>"_csrf=#{Rack::Utils.escape(token)}")}.must_raise Roda::RodaPlugins::RouteCsrf::InvalidToken
193
+ proc{body("/foo", "REQUEST_METHOD"=>'POST', 'rack.input'=>StringIO.new("_csrf=#{Rack::Utils.escape(token)}"))}.must_raise Roda::RodaPlugins::RouteCsrf::InvalidToken
194
+ end
195
+
196
+ it "supports csrf_tag method" do
197
+ route_csrf_app(:require_request_specific_tokens=>false){csrf_token}
198
+ body("/foo", "REQUEST_METHOD"=>'POST', 'rack.input'=>StringIO.new("_csrf=#{Rack::Utils.escape(body)}")).must_equal 'f'
199
+
200
+ route_csrf_app(:require_request_specific_tokens=>false, :field=>'foo'){csrf_token}
201
+ body("/foo", "REQUEST_METHOD"=>'POST', 'rack.input'=>StringIO.new("foo=#{Rack::Utils.escape(body)}")).must_equal 'f'
202
+
203
+ route_csrf_app{csrf_token('/foo')}
204
+ token = body
205
+ body("/foo", "REQUEST_METHOD"=>'POST', 'rack.input'=>StringIO.new("_csrf=#{Rack::Utils.escape(token)}")).must_equal 'f'
206
+ proc{body("/bar", "REQUEST_METHOD"=>'POST', 'rack.input'=>StringIO.new("_csrf=#{Rack::Utils.escape(token)}"))}.must_raise Roda::RodaPlugins::RouteCsrf::InvalidToken
207
+ proc{body("/foo", "REQUEST_METHOD"=>'PUT', 'rack.input'=>StringIO.new, 'QUERY_STRING'=>"_csrf=#{Rack::Utils.escape(token)}")}.must_raise Roda::RodaPlugins::RouteCsrf::InvalidToken
208
+
209
+ route_csrf_app do |r|
210
+ r.is 'foo', :method=>'PUT' do
211
+ 'f2'
212
+ end
213
+ csrf_token('/foo', 'PUT')
214
+ end
215
+ token = body
216
+ body("/foo", "REQUEST_METHOD"=>'PUT', 'rack.input'=>StringIO.new, 'QUERY_STRING'=>"_csrf=#{Rack::Utils.escape(token)}").must_equal 'f2'
217
+ proc{body("/bar", "REQUEST_METHOD"=>'PUT', 'rack.input'=>StringIO.new, 'QUERY_STRING'=>"_csrf=#{Rack::Utils.escape(token)}")}.must_raise Roda::RodaPlugins::RouteCsrf::InvalidToken
218
+ proc{body("/foo", "REQUEST_METHOD"=>'POST', 'rack.input'=>StringIO.new("_csrf=#{Rack::Utils.escape(token)}"))}.must_raise Roda::RodaPlugins::RouteCsrf::InvalidToken
219
+ end
220
+
221
+ it "supports csrf_path method" do
222
+ route_csrf_app do |r|
223
+ r.post{r.path + '2'}
224
+ csrf_token(csrf_path(env['CP']))
225
+ end
226
+
227
+ body("REQUEST_METHOD"=>'POST', 'rack.input'=>StringIO.new("_csrf=#{Rack::Utils.escape(body('CP'=>nil))}")).must_equal '/2'
228
+ body("REQUEST_METHOD"=>'POST', 'rack.input'=>StringIO.new("_csrf=#{Rack::Utils.escape(body('CP'=>''))}")).must_equal '/2'
229
+ body("REQUEST_METHOD"=>'POST', 'rack.input'=>StringIO.new("_csrf=#{Rack::Utils.escape(body('CP'=>'#foo'))}")).must_equal '/2'
230
+ body("REQUEST_METHOD"=>'POST', 'rack.input'=>StringIO.new("_csrf=#{Rack::Utils.escape(body('CP'=>'?foo'))}")).must_equal '/2'
231
+
232
+ body('/a', "REQUEST_METHOD"=>'POST', 'rack.input'=>StringIO.new("_csrf=#{Rack::Utils.escape(body('/a', 'CP'=>nil))}")).must_equal '/a2'
233
+ body('/a', "REQUEST_METHOD"=>'POST', 'rack.input'=>StringIO.new("_csrf=#{Rack::Utils.escape(body('/a', 'CP'=>''))}")).must_equal '/a2'
234
+ body('/a', "REQUEST_METHOD"=>'POST', 'rack.input'=>StringIO.new("_csrf=#{Rack::Utils.escape(body('/a', 'CP'=>'?foo'))}")).must_equal '/a2'
235
+ body('/a', "REQUEST_METHOD"=>'POST', 'rack.input'=>StringIO.new("_csrf=#{Rack::Utils.escape(body('/a', 'CP'=>'#foo'))}")).must_equal '/a2'
236
+
237
+ body("REQUEST_METHOD"=>'POST', 'rack.input'=>StringIO.new("_csrf=#{Rack::Utils.escape(body('CP'=>'http://foo/'))}")).must_equal '/2'
238
+ body('/a', "REQUEST_METHOD"=>'POST', 'rack.input'=>StringIO.new("_csrf=#{Rack::Utils.escape(body('CP'=>'https://foo/a'))}")).must_equal '/a2'
239
+ body('/a/b', "REQUEST_METHOD"=>'POST', 'rack.input'=>StringIO.new("_csrf=#{Rack::Utils.escape(body('CP'=>'http://foo/a/b'))}")).must_equal '/a/b2'
240
+
241
+ body("REQUEST_METHOD"=>'POST', 'rack.input'=>StringIO.new("_csrf=#{Rack::Utils.escape(body('CP'=>'/'))}")).must_equal '/2'
242
+ body('/a', "REQUEST_METHOD"=>'POST', 'rack.input'=>StringIO.new("_csrf=#{Rack::Utils.escape(body('CP'=>'/a'))}")).must_equal '/a2'
243
+ body('/a/b', "REQUEST_METHOD"=>'POST', 'rack.input'=>StringIO.new("_csrf=#{Rack::Utils.escape(body('CP'=>'/a/b'))}")).must_equal '/a/b2'
244
+
245
+ body('/a', "REQUEST_METHOD"=>'POST', 'rack.input'=>StringIO.new("_csrf=#{Rack::Utils.escape(body('HTTPS'=>'on', 'HTTP_HOST'=>'foo.com', 'CP'=>'a'))}")).must_equal '/a2'
246
+ body('/a', "REQUEST_METHOD"=>'POST', 'rack.input'=>StringIO.new("_csrf=#{Rack::Utils.escape(body('/b', 'HTTPS'=>'on', 'HTTP_HOST'=>'foo.com', 'CP'=>'a'))}")).must_equal '/a2'
247
+ body('/b/a', "REQUEST_METHOD"=>'POST', 'rack.input'=>StringIO.new("_csrf=#{Rack::Utils.escape(body('/b/', 'HTTPS'=>'on', 'HTTP_HOST'=>'foo.com', 'CP'=>'a'))}")).must_equal '/b/a2'
248
+ body('/b/a', "REQUEST_METHOD"=>'POST', 'rack.input'=>StringIO.new("_csrf=#{Rack::Utils.escape(body('/b/b', 'HTTPS'=>'on', 'HTTP_HOST'=>'foo.com', 'CP'=>'a'))}")).must_equal '/b/a2'
249
+ body('/a/b', "REQUEST_METHOD"=>'POST', 'rack.input'=>StringIO.new("_csrf=#{Rack::Utils.escape(body('/b', 'HTTPS'=>'on', 'HTTP_HOST'=>'foo.com', 'CP'=>'a/b'))}")).must_equal '/a/b2'
250
+ body('/b/a/b', "REQUEST_METHOD"=>'POST', 'rack.input'=>StringIO.new("_csrf=#{Rack::Utils.escape(body('/b/', 'HTTPS'=>'on', 'HTTP_HOST'=>'foo.com', 'CP'=>'a/b'))}")).must_equal '/b/a/b2'
251
+ body('/b/a/b', "REQUEST_METHOD"=>'POST', 'rack.input'=>StringIO.new("_csrf=#{Rack::Utils.escape(body('/b/a', 'HTTPS'=>'on', 'HTTP_HOST'=>'foo.com', 'CP'=>'a/b'))}")).must_equal '/b/a/b2'
252
+ end
253
+
254
+ begin
255
+ require 'rack/csrf'
256
+ rescue LoadError
257
+ warn "rack_csrf not installed, skipping route_csrf plugin test for rack_csrf upgrade"
258
+ else
259
+ it "supports upgrades from existing rack_csrf token" do
260
+ route_csrf_app(:upgrade_from_rack_csrf_key=>'csrf.token') do |r|
261
+ r.get 'clear' do
262
+ session.clear
263
+ ''
264
+ end
265
+ Rack::Csrf.token(env)
266
+ end
267
+ app.use Rack::Csrf, :skip=>['POST:/foo', 'POST:/bar'], :raise=>true
268
+ token = body
269
+ token.length.wont_equal 84
270
+ body("/foo", "REQUEST_METHOD"=>'POST', 'rack.input'=>StringIO.new("_csrf=#{Rack::Utils.escape(token)}")).must_equal 'f'
271
+ body("/bar", "REQUEST_METHOD"=>'POST', 'rack.input'=>StringIO.new("_csrf=#{Rack::Utils.escape(token)}")).must_equal 'b'
272
+ body('/clear').must_equal ''
273
+ proc{body("/foo", "REQUEST_METHOD"=>'POST', 'rack.input'=>StringIO.new("_csrf=#{Rack::Utils.escape(token)}"))}.must_raise Roda::RodaPlugins::RouteCsrf::InvalidToken
274
+ proc{body("/bar", "REQUEST_METHOD"=>'POST', 'rack.input'=>StringIO.new("_csrf=#{Rack::Utils.escape(token)}"))}.must_raise Roda::RodaPlugins::RouteCsrf::InvalidToken
275
+ end
276
+ end
277
+ end
@@ -187,7 +187,7 @@ describe "typecast_params plugin" do
187
187
  end
188
188
 
189
189
  it "#int should convert to integer" do
190
- tp('a=-1').int('a').must_equal -1
190
+ tp('a=-1').int('a').must_equal(-1)
191
191
  tp('a=0').int('a').must_equal 0
192
192
  tp('a=a').int('a').must_equal 0
193
193
  tp.int('a').must_equal 1
@@ -248,7 +248,7 @@ describe "typecast_params plugin" do
248
248
  end
249
249
 
250
250
  it "#Integer should convert to integer strictly" do
251
- tp('a=-1').Integer('a').must_equal -1
251
+ tp('a=-1').Integer('a').must_equal(-1)
252
252
  tp('a=0').Integer('a').must_equal 0
253
253
  lambda{tp('a=a').Integer('a')}.must_raise @tp_error
254
254
  tp.Integer('a').must_equal 1
@@ -277,7 +277,7 @@ describe "typecast_params plugin" do
277
277
  end
278
278
 
279
279
  it "#float should convert to float" do
280
- tp('a=-1').float('a').must_equal -1
280
+ tp('a=-1').float('a').must_equal(-1)
281
281
  tp('a=0').float('a').must_equal 0
282
282
  tp('a=a').float('a').must_equal 0
283
283
  tp.float('a').must_equal 1
@@ -306,7 +306,7 @@ describe "typecast_params plugin" do
306
306
  end
307
307
 
308
308
  it "#Float should convert to float strictly" do
309
- tp('a=-1').Float('a').must_equal -1
309
+ tp('a=-1').Float('a').must_equal(-1)
310
310
  tp('a=0').Float('a').must_equal 0
311
311
  lambda{tp('a=a').Float('a')}.must_raise @tp_error
312
312
  tp.Float('a').must_equal 1
@@ -1108,8 +1108,8 @@ describe "typecast_params plugin with customized params" do
1108
1108
  end
1109
1109
 
1110
1110
  it "should respect custom typecasting methods" do
1111
- tp.opp_int('a').must_equal -1
1112
- tp.opp_int!('a').must_equal -1
1111
+ tp.opp_int('a').must_equal(-1)
1112
+ tp.opp_int!('a').must_equal(-1)
1113
1113
  tp.opp_int('d').must_be_nil
1114
1114
  lambda{tp.opp_int!('d')}.must_raise @tp_error
1115
1115
 
@@ -1133,8 +1133,8 @@ describe "typecast_params plugin with customized params" do
1133
1133
  end
1134
1134
  end
1135
1135
 
1136
- tp.opp_int('a').must_equal -1
1137
- tp.opp_int!('a').must_equal -1
1136
+ tp.opp_int('a').must_equal(-1)
1137
+ tp.opp_int!('a').must_equal(-1)
1138
1138
  tp.opp_int('d').must_be_nil
1139
1139
  lambda{tp.opp_int!('d')}.must_raise @tp_error
1140
1140
 
data/spec/session_spec.rb CHANGED
@@ -1,6 +1,8 @@
1
1
  require_relative "spec_helper"
2
2
 
3
3
  describe "session handling" do
4
+ include CookieJar
5
+
4
6
  it "should give a warning if session variable is not available" do
5
7
  app do |r|
6
8
  begin
@@ -27,9 +29,9 @@ describe "session handling" do
27
29
 
28
30
  _, h, b = req
29
31
  b.join.must_equal 'ab'
30
- _, h, b = req('HTTP_COOKIE'=>h['Set-Cookie'].sub("; path=/; HttpOnly", ''))
32
+ _, h, b = req
31
33
  b.join.must_equal 'abb'
32
- _, h, b = req('HTTP_COOKIE'=>h['Set-Cookie'].sub("; path=/; HttpOnly", ''))
34
+ _, h, b = req
33
35
  b.join.must_equal 'abbb'
34
36
  end
35
37
  end
data/spec/spec_helper.rb CHANGED
@@ -24,6 +24,8 @@ end
24
24
 
25
25
  require_relative "../lib/roda"
26
26
  require "stringio"
27
+
28
+ ENV['MT_NO_PLUGINS'] = '1' # Work around stupid autoloading of plugins
27
29
  gem 'minitest'
28
30
  require "minitest/autorun"
29
31
 
@@ -34,6 +36,23 @@ def (Roda::RodaPlugins).warn(s)
34
36
  puts caller.grep(/_spec\.rb:\d+:/)
35
37
  end
36
38
 
39
+ module CookieJar
40
+ def req(path='/', env={})
41
+ if path.is_a?(Hash)
42
+ env = path
43
+ else
44
+ env['PATH_INFO'] = path.dup
45
+ end
46
+ env['HTTP_COOKIE'] = @cookie if @cookie
47
+
48
+ a = super(env)
49
+ if set = a[1]['Set-Cookie']
50
+ @cookie = set.sub("; path=/; HttpOnly", '')
51
+ end
52
+ a
53
+ end
54
+ end
55
+
37
56
  class Minitest::Spec
38
57
  def self.deprecated(a, &block)
39
58
  it("#{a} (deprecated)") do
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: roda
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.8.0
4
+ version: 3.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jeremy Evans
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-05-17 00:00:00.000000000 Z
11
+ date: 2018-06-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rack
@@ -204,6 +204,7 @@ extra_rdoc_files:
204
204
  - doc/release_notes/3.5.0.txt
205
205
  - doc/release_notes/3.6.0.txt
206
206
  - doc/release_notes/3.8.0.txt
207
+ - doc/release_notes/3.9.0.txt
207
208
  files:
208
209
  - CHANGELOG
209
210
  - MIT-LICENSE
@@ -254,6 +255,7 @@ files:
254
255
  - doc/release_notes/3.6.0.txt
255
256
  - doc/release_notes/3.7.0.txt
256
257
  - doc/release_notes/3.8.0.txt
258
+ - doc/release_notes/3.9.0.txt
257
259
  - lib/roda.rb
258
260
  - lib/roda/plugins/_symbol_regexp_matchers.rb
259
261
  - lib/roda/plugins/all_verbs.rb
@@ -322,6 +324,7 @@ files:
322
324
  - lib/roda/plugins/request_aref.rb
323
325
  - lib/roda/plugins/request_headers.rb
324
326
  - lib/roda/plugins/response_request.rb
327
+ - lib/roda/plugins/route_csrf.rb
325
328
  - lib/roda/plugins/run_append_slash.rb
326
329
  - lib/roda/plugins/run_handler.rb
327
330
  - lib/roda/plugins/shared_vars.rb
@@ -419,6 +422,7 @@ files:
419
422
  - spec/plugin/request_aref_spec.rb
420
423
  - spec/plugin/request_headers_spec.rb
421
424
  - spec/plugin/response_request_spec.rb
425
+ - spec/plugin/route_csrf_spec.rb
422
426
  - spec/plugin/run_append_slash_spec.rb
423
427
  - spec/plugin/run_handler_spec.rb
424
428
  - spec/plugin/shared_vars_spec.rb
@@ -484,7 +488,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
484
488
  version: '0'
485
489
  requirements: []
486
490
  rubyforge_project:
487
- rubygems_version: 2.7.6
491
+ rubygems_version: 3.0.0.beta1
488
492
  signing_key:
489
493
  specification_version: 4
490
494
  summary: Routing tree web toolkit