lib/roda/plugins/route_csrf.rb in roda-3.76.0 vs lib/roda/plugins/route_csrf.rb in roda-3.77.0

- old
+ new

@@ -40,10 +40,13 @@ # plugin :route_csrf # # This plugin supports the following options: # # :field :: Form input parameter name for CSRF token (default: '_csrf') + # :formaction_field :: Form input parameter name for path-specific CSRF tokens (used by the + # +csrf_formaction_tag+ method). If present, this parameter should be + # submitted as a hash, keyed by path, with CSRF token values. # :header :: HTTP header name for CSRF token (default: 'X-CSRF-Token') # :key :: Session key for CSRF secret (default: '_roda_csrf_secret') # :require_request_specific_tokens :: Whether request-specific tokens are required (default: true). # A false value will allow tokens that are not request-specific # to also work. You should only set this to false if it is @@ -84,10 +87,14 @@ # token is necessary for the request and there is no token provided # or the provided token is not valid. Options can be provided to # override any of the plugin options for this specific call. # The :token option can be used to specify the provided CSRF token # (instead of looking for the token in the submitted parameters). + # csrf_formaction_tag(path, method='POST') :: An HTML hidden input tag string containing the CSRF token, suitable + # for placing in an HTML form that has inputs that use formaction + # attributes to change the endpoint to which the form is submitted. + # Takes the same arguments as csrf_token. # csrf_field :: The field name to use for the hidden tag containing the CSRF token. # csrf_path(action) :: This takes an argument that would be the value of the HTML form's # action attribute, and returns a path you can pass to csrf_token # that should be valid for the form submission. The argument should # either be nil or a string representing a relative path, absolute @@ -150,10 +157,11 @@ # each time, mitigating compression ratio attacks such as BREACH. module RouteCsrf # Default CSRF option values DEFAULTS = { :field => '_csrf'.freeze, + :formaction_field => '_csrfs'.freeze, :header => 'X-CSRF-Token'.freeze, :key => '_roda_csrf_secret'.freeze, :require_request_specific_tokens => true, :csrf_failure => :raise, :check_header => false, @@ -250,10 +258,18 @@ # relative path, join to current path URI.join(request.url, action).path end end + # An HTML hidden input tag string containing the CSRF token, used for inputs + # with formaction, so the same form can be used to submit to multiple endpoints + # depending on which button was clicked. See csrf_token for arguments, but the + # path argument is required. + def csrf_formaction_tag(path, *args) + "<input type=\"hidden\" name=\"#{csrf_options[:formaction_field]}[#{Rack::Utils.escape_html(path)}]\" value=\"#{csrf_token(path, *args)}\" \/>" + end + # An HTML hidden input tag string containing the CSRF token. See csrf_token for # arguments. def csrf_tag(*args) "<input type=\"hidden\" name=\"#{csrf_field}\" value=\"#{csrf_token(*args)}\" \/>" end @@ -289,18 +305,21 @@ unless opts[:check_request_methods].include?(method) return end + path = @_request.path + unless encoded_token = opts[:token] encoded_token = case opts[:check_header] when :only env[opts[:env_header]] when true return (csrf_invalid_message(opts.merge(:check_header=>false)) && csrf_invalid_message(opts.merge(:check_header=>:only))) else - @_request.params[opts[:field]] + params = @_request.params + ((formactions = params[opts[:formaction_field]]).is_a?(Hash) && (formactions[path])) || params[opts[:field]] end end unless encoded_token.is_a?(String) return "encoded token is not a string" @@ -324,10 +343,10 @@ return "encoded token is not valid base64" end random_data = submitted_hmac.slice!(0...31) - if csrf_compare(csrf_hmac(random_data, method, @_request.path), submitted_hmac) + if csrf_compare(csrf_hmac(random_data, method, path), submitted_hmac) return end if opts[:require_request_specific_tokens] "decoded token is not valid for request method and path"