roda 3.8.0 → 3.9.0

Sign up to get free protection for your applications and to get access to all the features.
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