Module: Roda::RodaPlugins::RouteCsrf
- Defined in:
- lib/roda/plugins/route_csrf.rb
Overview
The route_csrf plugin is the recommended plugin to use to support CSRF protection in Roda applications. This plugin allows you set where in the routing tree to enforce CSRF protection. Additionally, the route_csrf plugin uses modern security practices.
By default, the plugin requires tokens be specific to the request method and request path, so a CSRF token generated for one form will not be usable to submit a different form.
This plugin also takes care to not expose the underlying CSRF key (except in the session), so that it is not possible for an attacker to generate valid CSRF tokens specific to an arbitrary request method and request path even if they have access to a token that is not specific to request method and request path. To get this security benefit, you must ensure an attacker does not have access to the session. Rack::Session::Cookie versions shipped with Rack before Rack 3 use signed sessions, not encrypted sessions, so if the attacker has the ability to read cookie data and you are using one of those Rack::Session::Cookie versions, it will still be possible for an attacker to generate valid CSRF tokens specific to arbitrary request method and request path. Roda’s session plugin uses encrypted sessions and therefore is safe even if the attacker can read cookie data.
Usage
It is recommended to use the plugin defaults, loading the plugin with no options:
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 impossible to use request-specific tokens. If you must use non-request-specific tokens in certain cases, it is best to leave this option true by default, and override it on a per call basis in those specific cases.
- :csrf_failure
-
The action to taken if a request fails the CSRF check (default: :raise). Options:
- :raise
-
raise a Roda::RodaPlugins::RouteCsrf::InvalidToken exception
- :empty_403
-
return a blank 403 page (rack_csrf’s default behavior)
- :clear_session
-
Clear the current session
- Proc
-
Treated as a routing block, called with request object
- :check_header
-
Whether the HTTP header should be checked for the token value (default: false). If true, checks the HTTP header after checking for the form input parameter. If :only, only checks the HTTP header and doesn’t check the form input parameter.
- :check_request_methods
-
Which request methods require CSRF protection (default:
['POST', 'DELETE', 'PATCH', 'PUT']
) - :upgrade_from_rack_csrf_key
-
If provided, the session key that should be checked for the rack_csrf raw token. If the session key is present, the value will be checked against the submitted token, and if it matches, the CSRF check will be passed. Should only be set temporarily if upgrading from using rack_csrf to the route_csrf plugin, and should be removed as soon as you are OK with CSRF forms generated before the upgrade not longer being usable. The default rack_csrf key is
'csrf.token'
.
The plugin also supports a block, in which case the block will be used as the value of the :csrf_failure option.
Methods
This adds the following instance methods:
- check_csrf!(opts={})
-
Used for checking if the submitted CSRF token is valid. If a block is provided, it is treated as a routing block if the CSRF token is not valid. Otherwise, by default, raises a Roda::RodaPlugins::RouteCsrf::InvalidToken exception if a CSRF 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 path, or full URL (using appropriate URL encoding).
- csrf_tag(path=nil, method=‘POST’)
-
An HTML hidden input tag string containing the CSRF token, suitable for placing in an HTML form. Takes the same arguments as csrf_token.
- csrf_token(path=nil, method=‘POST’)
-
The value of the csrf token, in case it needs to be accessed directly. It is recommended to call this method with a path, which will create a request-specific token. Calling this method without an argument will create a token that is not specific to the request, but such a token will only work if you set the :require_request_specific_tokens option to false, which is a bad idea from a security standpoint.
- use_request_specific_csrf_tokens?
-
Whether the plugin is configured to only support request-specific tokens, true by default.
- valid_csrf?(opts={})
-
Returns whether the submitted CSRF token is valid (also true if the request does not require a CSRF token). Takes same option hash as check_csrf!.
This plugin also adds the following instance methods for compatibility with the older csrf plugin, but it is not recommended to use these methods in new code:
- csrf_header
-
The header name to use for submitting the CSRF token via an HTTP header (useful for javascript). Note that this plugin will not look in the HTTP header by default, it will only do so if the :check_header option is used.
- csrf_metatag
-
An HTML meta tag string containing the CSRF token, suitable for placing in the page header. It is not recommended to use this method, as the token generated is not request-specific and will not work unless you set the :require_request_specific_tokens option to false, which is a bad idea from a security standpoint.
Token Cryptography
route_csrf uses HMAC-SHA-256 to generate all CSRF tokens. It generates a random 32-byte secret, which is stored base64 encoded in the session. For each CSRF token, it generates 31 bytes of random data.
For request-specific CSRF tokens, this pseudocode generates the HMAC:
hmac = HMAC(secret, method + path + random_data)
For CSRF tokens not specific to a request, this pseudocode generates the HMAC:
hmac = HMAC(secret, random_data)
This pseudocode generates the final CSRF token in both cases:
token = Base64Encode(random_data + hmac)
Using this construction for generating CSRF tokens means that generating any valid CSRF token without knowledge of the secret is equivalent to a successful generic attack on HMAC-SHA-256.
By using an HMAC for tokens not specific to a request, it is not possible to use a valid CSRF token that is not specific to a request to generate a valid request-specific CSRF token.
By including random data in the HMAC for all tokens, different tokens are generated each time, mitigating compression ratio attacks such as BREACH.
Defined Under Namespace
Modules: InstanceMethods Classes: InvalidToken
Constant Summary collapse
- DEFAULTS =
Default CSRF option values
{ :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, :check_request_methods => %w'POST DELETE PATCH PUT'.freeze.each(&:freeze) }.freeze
Class Method Summary collapse
Class Method Details
.configure(app, opts = OPTS, &block) ⇒ Object
179 180 181 182 183 184 185 186 187 188 189 190 191 |
# File 'lib/roda/plugins/route_csrf.rb', line 179 def self.configure(app, opts=OPTS, &block) = app.opts[:route_csrf] = (app.opts[:route_csrf] || DEFAULTS).merge(opts) if block || opts[:csrf_failure].is_a?(Proc) if block && opts[:csrf_failure] raise RodaError, "Cannot specify both route_csrf plugin block and :csrf_failure option" end block ||= opts[:csrf_failure] [:csrf_failure] = :csrf_failure_method app.define_roda_method(:_roda_route_csrf_failure, 1, &app.send(:convert_route_block, block)) end [:env_header] = "HTTP_#{[:header].to_s.gsub('-', '_').upcase}".freeze .freeze end |
.load_dependencies(app, opts = OPTS, &_) ⇒ Object
175 176 177 |
# File 'lib/roda/plugins/route_csrf.rb', line 175 def self.load_dependencies(app, opts=OPTS, &_) app.plugin :_base64 end |