Controllers
CSRF Protection
Cross-site request forgery (CSRF) protection stops other sites from submitting
forms to your app on behalf of a logged-in user. Padrino adds
Rack::Protection::AuthenticityToken to an app when protect_from_csrf is on.
Every request that is not a GET, HEAD, OPTIONS or TRACE must then carry a token
that matches the one stored in the session, or it is rejected with
403 Forbidden.
Turning it on
Protection is off in a bare Padrino::Application. New projects turn it on for
every app in config/apps.rb:
Padrino.configure_apps do
set :session_secret, '...'
set :protection, except: :path_traversal
set :protect_from_csrf, Padrino.env != :test
end
You can also set it per app:
module Demo
class App < Padrino::Application
enable :sessions
enable :protect_from_csrf
end
end
The token lives in the session, so CSRF protection needs sessions. Generated
apps turn them on with set :sessions, same_site: :lax. If sessions are off,
Padrino prints a warning and every request, GETs included, is rejected with
403 Forbidden.
To use another session store, pass it to the sessions setting so that Padrino
adds it before the CSRF check:
set :sessions, use: Rack::Session::Pool, config: { same_site: :lax, expire_after: 2592000 }
Middleware added with use inside the app runs after the CSRF check, so every
request is rejected. If you add the session middleware outside Padrino, in
config.ru, silence the warning in config/boot.rb:
Padrino::IGNORE_CSRF_SETUP_WARNING = true
Forms
The form helpers add the token for you. form_tag, form_for and button_to
render a hidden authenticity_token field in every form that does not use the
GET method:
<% form_tag '/register' do %>
<%= text_field_tag :email %>
<%= submit_tag 'Sign up' %>
<% end %>
<form action="/register" accept-charset="UTF-8" method="post">
<input type="hidden" name="authenticity_token" value="..." />
...
</form>
Use button_to for links that change data, such as a delete link. It renders a
small form with the token.
To leave the token out of one form, pass protect_from_csrf: false:
<% form_tag '/search', protect_from_csrf: false do %>
If you write a form by hand, add the field with csrf_token_field.
JavaScript requests
Put the token in your layout with csrf_meta_tags:
<head>
<%= csrf_meta_tags %>
</head>
It renders csrf-param and csrf-token meta tags. Send the token back in the
X-CSRF-Token header:
const token = document.querySelector('meta[name="csrf-token"]').content
fetch('/items', { method: 'POST', headers: { 'X-CSRF-Token': token } })
Skipping routes
To skip the check for some paths, such as a webhook, pass except. It takes a
string, a regular expression, an array of them, or a proc that receives the Rack
env. Paths are matched inside the app, without its mount point:
set :protect_from_csrf, except: %r{\A/webhooks/}
To opt out route by route, enable allow_disabled_csrf and pass
csrf_protection: false to the route:
enable :allow_disabled_csrf
post :callback, csrf_protection: false do
# no token needed
end
Without allow_disabled_csrf, csrf_protection: false has no effect.
With allow_disabled_csrf (or report_csrf_failure, which does the same), the
middleware only flags a failed check and each route rejects it with a 403. The
response body is Forbidden, or the message you set:
set :protect_from_csrf, message: 'Invalid form token'
enable :allow_disabled_csrf
Set these before your routes are defined, for example in app/app.rb or
config/apps.rb. Routes defined before allow_disabled_csrf is enabled are
not checked at all.
A hash value for protect_from_csrf turns protection on in every environment,
replacing the generated Padrino.env != :test. To keep tests free of tokens,
choose the value by environment:
set :protect_from_csrf, Padrino.env == :test ? false : { except: %r{\A/webhooks/} }
Testing
The generated config/apps.rb turns protection off in the test environment, so
test requests don't need a token and forms are rendered without the hidden
field.
The app reads protect_from_csrf when it builds its middleware, on its first
request, so turning it on inside one test does nothing if an earlier test has
already made a request. To test the protection itself, turn it on before the
app is loaded and run those tests in their own process. For example, use
set :protect_from_csrf, Padrino.env != :test || ENV['CSRF'] == '1' in
config/apps.rb, and run the CSRF tests with CSRF=1. Get the form, then post
the authenticity_token back with the session cookie.
rake test loads every test file, so skip the CSRF tests when CSRF isn't set:
before { skip 'run with CSRF=1' unless ENV['CSRF'] == '1' }
Run them on their own, since CSRF=1 turns protection on for every test in the
process:
$ CSRF=1 bundle exec ruby -Itest test/app/csrf_test.rb