A single missing CSRF token can let an attacker transfer funds, change passwords, or delete data on behalf of a logged-in user. Rails provides strong CSRF protection out of the box, but misconfiguration, skipped verifications, or unprotected AJAX calls create gaps. This checklist covers every layer of defense.
What is CSRF and why does Rails protect against it?
Cross-Site Request Forgery tricks a user’s browser into making unwanted requests to a site where they are authenticated. The attacker crafts a malicious page that submits a form or fires a request to your app. Since the browser automatically includes session cookies, Rails sees a valid session and executes the action.
Rails defends against this by generating a unique authenticity token per session and verifying it on every non-GET request. If the token is missing or wrong, Rails rejects the request.
Rails Cross-Site Request Forgery (CSRF)

How do you enable CSRF protection?
protect_from_forgery in ApplicationController
One line enables CSRF verification across your entire app:
class ApplicationController < ActionController::Base
protect_from_forgery with: :exception
end
Every controller inheriting from ApplicationController automatically verifies authenticity tokens on POST, PUT, PATCH, and DELETE requests.
Which failure strategy should you pick?
| Strategy | Behavior | Best for |
|---|---|---|
:exception | Raises ActionController::InvalidAuthenticityToken | Standard web apps with strict security |
:null_session | Returns an empty session without resetting | Hybrid apps serving both web and API |
:reset_session | Wipes the session completely | Apps requiring maximum session security |
:exception is the right default for most web applications. It stops the attack, logs the incident, and lets you monitor attempts. :null_session works for hybrid apps where API endpoints coexist with session-based web controllers.
Adding csrf_meta_tags to your layout
For JavaScript-driven requests to work, CSRF tokens must be available in the DOM. Add this to the <head> section of application.html.erb:
<%= csrf_meta_tags %>
This generates two meta tags:
<meta name="csrf-param" content="authenticity_token" />
<meta name="csrf-token" content="your_token_value" />
Without these, every AJAX request from your JavaScript will be rejected by Rails.
CSRF protection checklist
1. Always use Rails form helpers
Rails form helpers automatically embed the CSRF token as a hidden field:
<%= form_with model: @project do |form| %>
<%= form.label :name %>
<%= form.text_field :name %>
<%= form.submit "Save" %>
<% end %>
If you must use plain HTML forms, manually include the token:
<%= hidden_field_tag :authenticity_token, form_authenticity_token %>
2. Never use GET for state-changing actions
GET requests bypass CSRF verification entirely. A link like <a href="/users/123/delete"> could delete a user account if the route accepts GET.
| HTTP Method | Purpose | CSRF Protected? | Example |
|---|---|---|---|
| GET | Read data | No | Viewing profiles, listing items |
| POST | Create resources | Yes | User registration, new posts |
| PUT/PATCH | Update resources | Yes | Editing profiles, updating settings |
| DELETE | Remove resources | Yes | Deleting accounts, removing posts |
3. Audit skip_forgery_protection usage
Search your codebase for skip_forgery_protection and skip_before_action :verify_authenticity_token. Document every instance. Legitimate cases include:
- API controllers using token-based authentication (JWT, OAuth)
- Webhook endpoints receiving callbacks from trusted services
- Public endpoints that never handle sensitive data
If an exception is no longer justified, restore CSRF protection immediately.
4. Configure SameSite cookies
The SameSite attribute prevents browsers from sending cookies with cross-site requests:
Rails.application.config.session_store :cookie_store,
key: '_your_app_session',
same_site: :lax
:lax blocks most cross-origin requests while allowing legitimate top-level navigation (e.g., clicking a link from another site). :strict blocks all cross-origin cookie transmission but may break flows where users arrive from external links.
5. Fix XSS vulnerabilities
XSS and CSRF are related attacks. If an attacker injects JavaScript via XSS, that script can read CSRF tokens from meta tags or hidden fields and forge valid requests.
Prevent XSS to protect CSRF:
- Sanitize user input before storing it
- Escape HTML output (Rails does this by default in ERB)
- Implement Content Security Policy headers to block inline scripts
- Audit high-risk areas: search results, comment sections, user-provided HTML
A single XSS vulnerability can completely bypass CSRF protection.
How do you handle CSRF in API-only apps?
API-only controllers using stateless authentication (JWT, API tokens, OAuth) do not need CSRF protection. Since no cookies are sent automatically, the CSRF attack vector does not apply.
class Api::V1::BaseController < ApplicationController
skip_before_action :verify_authenticity_token
end
For hybrid apps, keep CSRF enabled for web controllers and skip it only for API controllers. Ensure API endpoints validate tokens on every request using a before_action.
How do you protect AJAX requests?
JavaScript requests do not automatically include CSRF tokens. Read the token from the meta tag and attach it as a header:
const token = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
fetch('/posts', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': token
},
body: JSON.stringify({ post: { title: 'New Post' } }),
credentials: 'same-origin'
});
Angular handles CSRF automatically via the cookie-to-header pattern. For React or Vue, use axios interceptors to attach the token to every non-GET request.
Always include credentials: 'same-origin' so the browser sends session cookies along with the request.
How do you prevent token exposure?
CSRF tokens must never appear in URLs. Tokens in URLs leak through browser history, referrer headers, server logs, and shared links.
Secure token storage:
Rails.application.config.session_store :cookie_store,
key: '_app_session',
same_site: :strict,
secure: true,
httponly: true
Additional precautions:
- Never log request headers containing CSRF tokens
- Never store tokens in
localStorageorsessionStorage - Use separate authentication mechanisms for webhook and third-party integrations
- Audit that tokens do not appear in error messages or debugging output
Practical Implementation: The USEO Approach
At USEO, we have hardened CSRF protection across Rails apps handling financial data, healthcare records, and enterprise workflows. Here is what we have learned:
Automated CSRF auditing in CI. We run a custom RSpec matcher in our CI pipeline that scans every controller for skip_forgery_protection. If a new skip is added without an accompanying comment explaining why, the build fails. This prevents accidental CSRF disabling during feature development.
Token rotation on privilege escalation. We regenerate CSRF tokens (via reset_session + re-authentication) whenever a user elevates privileges: logging in, changing passwords, or accessing admin panels. This limits the window in which a stolen token is useful.
Double-submit cookie pattern for SPAs. For single-page applications where csrf_meta_tags are only available on initial page load, we implement the double-submit pattern. Rails sets the CSRF token in a non-HttpOnly cookie. The JavaScript reads it and sends it back in a custom header. Rails verifies the header matches the cookie. This works across page navigations without requiring a full page reload.
Content Security Policy as a CSRF backstop. We deploy strict CSP headers (script-src 'self') on every client project. Even if an XSS vulnerability slips through, CSP prevents the injected script from executing, which means it cannot read CSRF tokens. CSP is not a replacement for proper escaping, but it is a valuable defense-in-depth layer.
Quarterly security reviews. We schedule quarterly audits where we grep for skip_forgery_protection, review cookie configurations, and test CSRF flows manually with tools like Burp Suite. We also verify that new third-party JavaScript libraries have not introduced XSS vectors that could compromise CSRF defenses.
Final CSRF protection checklist
Foundation
protect_from_forgeryenabled inApplicationControllercsrf_meta_tagspresent in application layout- CSRF tokens generate and validate correctly
Forms and requests
- All state-changing forms use Rails form helpers
- No GET routes modify data
- Every
skip_forgery_protectionis documented and justified
Cookies and sessions
- Session cookies use
SameSite,Secure, andHttpOnlyattributes - Cookie configuration verified in production environment
JavaScript and AJAX
- All AJAX requests include
X-CSRF-Tokenheader - SPA token handling tested across navigation flows
- Frontend frameworks configured for cookie-to-header pattern
Security validation
- No XSS vulnerabilities that could expose tokens
- Tokens never appear in URLs, logs, or referrer headers
- Automated security scanning in CI pipeline
Team process
- Developers trained on CSRF attack vectors
- CSRF checks included in code review checklist
- Exceptions documented with justification
FAQs
How do you handle CSRF in a Rails app with both web pages and API endpoints?
Keep CSRF enabled for web controllers and skip it for API controllers that use stateless authentication (JWT, OAuth, API keys). Create a base API controller with skip_before_action :verify_authenticity_token and have all API controllers inherit from it. Web controllers continue inheriting from ApplicationController with full CSRF protection.
What risks come with disabling CSRF protection?
Disabling CSRF allows attackers to perform actions as authenticated users: transferring money, changing email addresses, deleting data. Only disable CSRF for controllers using token-based authentication where cookies are not involved. Always audit skip_forgery_protection usage and remove exceptions that are no longer needed.
How does the SameSite cookie attribute complement CSRF tokens?
SameSite prevents the browser from sending cookies with cross-origin requests, blocking the primary CSRF attack vector at the browser level. Combined with token verification, you get two independent layers of protection. Use SameSite: Lax for most apps or SameSite: Strict for maximum security when cross-site navigation is not needed.