Overview
Cross-Site Request Forgery (CSRF) attacks exploit the trust a web application has in a user's browser. When a user authenticates with a web application, the browser stores credentials (typically session cookies) and automatically includes them with subsequent requests to that domain. CSRF attacks abuse this automatic credential inclusion by tricking the user's browser into making requests the user did not intend.
The attack succeeds because web applications cannot distinguish between requests the user intended to make and requests triggered by malicious third-party sites. If a user visits an attacker's website while authenticated to a target application, the attacker can craft requests that the browser will automatically authenticate using the victim's valid session.
CSRF differs from Cross-Site Scripting (XSS) attacks. XSS involves injecting malicious scripts into a trusted site, while CSRF tricks the browser into making requests from an external site. CSRF attacks require the victim to be authenticated and typically target state-changing operations like fund transfers, password changes, or email updates.
The impact ranges from minor annoyances to severe security breaches. An attacker could transfer funds, modify account settings, make purchases, or execute any action the authenticated user has permission to perform. The attack requires no malware installation and leaves minimal traces, making detection difficult.
# Vulnerable endpoint - accepts GET request for state change
class BankController < ApplicationController
def transfer
account = params[:to_account]
amount = params[:amount]
current_user.transfer_funds(account, amount)
redirect_to dashboard_path
end
end
# Attacker's malicious page
# <img src="https://bank.com/transfer?to_account=attacker&amount=1000">
# User's browser automatically includes session cookie
Key Principles
CSRF attacks succeed through three fundamental mechanisms: automatic credential inclusion, lack of request origin verification, and execution of state-changing operations. Understanding these principles clarifies both the attack vector and defense strategies.
Automatic Credential Inclusion
Browsers automatically include credentials (cookies, HTTP authentication headers) with every request to a domain, regardless of where the request originated. This behavior enables single sign-on and session persistence but creates the CSRF vulnerability. When a user loads a page containing a malicious request, the browser dutifully attaches valid credentials.
# User authenticated to bank.com
# Session cookie: session_id=abc123
# Attacker's site triggers request
# Browser automatically includes: Cookie: session_id=abc123
# Bank application sees valid session and processes request
Same-Origin Policy Limitations
The Same-Origin Policy (SOP) prevents JavaScript on one origin from reading responses from another origin. However, SOP does not prevent requests from being sent—it only restricts reading the response. The browser will still execute the request with credentials, and state changes occur on the server even if the attacker cannot read the response.
# Attacker's JavaScript (on evil.com)
fetch('https://bank.com/transfer', {
method: 'POST',
credentials: 'include',
body: 'to_account=attacker&amount=1000'
})
# Request sent with cookies
# Response blocked by SOP
# But damage already done - transfer executed
State-Changing Operations
CSRF attacks target operations that modify server state. Read-only operations pose less risk because the attacker cannot access the response due to SOP. State-changing operations include creating records, updating data, deleting resources, changing configurations, or triggering workflows.
Request Verification
Effective CSRF protection requires verifying that requests originated from the application itself, not from external sites. This verification uses secrets that attackers cannot access or predict. The server includes an unpredictable token in forms and validates it on submission. Attackers on external domains cannot read this token due to SOP.
HTTP Method Semantics
The HTTP specification defines GET as safe and idempotent—it should not modify server state. Applications that use GET for state changes become more vulnerable to CSRF because attackers can trigger GET requests through simple HTML elements like images or links, without requiring form submission or JavaScript.
# Vulnerable - GET modifies state
get '/delete_account' do
current_user.destroy
end
# Attacker triggers with image tag
# <img src="https://app.com/delete_account">
# Safer - POST requires form or JavaScript
post '/delete_account' do
current_user.destroy
end
# Cannot trigger with simple HTML element
Security Implications
CSRF vulnerabilities expose applications to unauthorized actions executed with legitimate user credentials. The attack requires no special access beyond tricking an authenticated user into visiting a malicious page, making it accessible to relatively unsophisticated attackers.
Authentication Bypass
CSRF bypasses authentication by abusing existing valid sessions. The application correctly verifies the user's identity through session cookies but fails to verify request intent. From the server's perspective, the malicious request appears identical to legitimate user actions.
Privilege Escalation
When CSRF targets administrative functions, attackers can perform privileged operations. An attacker who tricks an administrator into triggering a request can create new admin accounts, modify permissions, or access sensitive data. The attack inherits all privileges of the authenticated victim.
# Admin function vulnerable to CSRF
class AdminController < ApplicationController
before_action :require_admin
def create_admin
User.create!(
email: params[:email],
admin: true
)
end
end
# Attacker creates admin account
# <form action="https://app.com/admin/create_admin" method="POST">
# <input name="email" value="attacker@evil.com">
# </form>
# <script>document.forms[0].submit()</script>
Data Exfiltration Through Side Effects
While attackers cannot read CSRF response data, they can infer information through observable side effects. Timing differences, error messages leaked through redirects, or changes in application behavior reveal information about the operation's success or failure.
API Endpoint Exposure
JSON APIs face CSRF risks when they accept cookie-based authentication. Single-page applications that authenticate with cookies remain vulnerable. The Content-Type header provides partial protection since simple requests can use application/x-www-form-urlencoded, but JSON requests require a preflight check.
# JSON API vulnerable if using cookie auth
class ApiController < ApplicationController
def update_profile
current_user.update!(profile_params)
render json: { status: 'success' }
end
end
# Attacker can still modify data
# Response blocked by SOP but state change occurs
Subdomain Attacks
CSRF protection often relies on validating the Origin or Referer headers. However, compromised subdomains or permissive CORS policies can weaken these defenses. An attacker controlling a subdomain can set document.domain to match the parent domain in some scenarios.
Mobile Application Risks
Mobile applications using WebViews with cookie-based authentication inherit CSRF vulnerabilities. The WebView automatically includes cookies, and users may not realize they're viewing untrusted content within the application context.
Ruby Implementation
Rails provides built-in CSRF protection through the synchronizer token pattern. The framework generates unique tokens per session and validates them on state-changing requests. This implementation operates transparently for most applications but requires understanding for proper configuration.
Default Protection Mechanism
Rails includes CSRF protection automatically in ApplicationController through the protect_from_forgery method. The framework generates an authenticity token stored in the session and embedded in forms. On POST, PUT, PATCH, and DELETE requests, Rails validates the submitted token against the session value.
class ApplicationController < ActionController::Base
# Default protection enabled
protect_from_forgery with: :exception
# Alternative strategies:
# protect_from_forgery with: :null_session # Clear session on failure
# protect_from_forgery with: :reset_session # Reset entire session
end
Token Generation and Validation
Rails automatically injects CSRF tokens into forms through form helpers. The form_with and form_for helpers include a hidden field containing the authenticity token. For manual forms or AJAX requests, access the token through form_authenticity_token.
# Automatic token injection
<%= form_with model: @user do |f| %>
<%= f.text_field :name %>
<%= f.submit %>
<% end %>
# Renders: <input type="hidden" name="authenticity_token" value="...">
# Manual token inclusion
<form action="/users" method="post">
<input type="hidden" name="authenticity_token"
value="<%= form_authenticity_token %>">
<input type="text" name="user[name]">
<input type="submit">
</form>
# JavaScript access for AJAX
document.querySelector('meta[name="csrf-token"]').content
Meta Tag Configuration
Rails includes the CSRF token in a meta tag for JavaScript access. AJAX libraries can read this token and include it in request headers. The UJS (Unobtrusive JavaScript) library handles this automatically for Rails-generated AJAX requests.
# Application layout
<head>
<%= csrf_meta_tags %>
</head>
# Renders: <meta name="csrf-token" content="...">
# JavaScript AJAX request
const token = document.querySelector('meta[name="csrf-token"]').content
fetch('/api/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': token
},
body: JSON.stringify({ name: 'Alice' })
})
API Controller Configuration
API controllers typically use token-based authentication (JWT, OAuth) instead of session cookies, making them immune to CSRF attacks. Disable CSRF protection for API-only controllers to avoid validation errors.
class Api::BaseController < ActionController::API
# ActionController::API excludes CSRF protection by default
# No protect_from_forgery needed
end
# Mixed controllers serving both HTML and JSON
class UsersController < ApplicationController
protect_from_forgery with: :exception, unless: -> { request.format.json? }
# Or skip for specific actions
skip_before_action :verify_authenticity_token, only: [:webhook]
end
Custom Token Validation
Applications with complex authentication schemes may need custom CSRF validation logic. Override verified_request? to implement custom verification while maintaining Rails' protection framework.
class ApplicationController < ActionController::Base
protect_from_forgery with: :exception
private
def verified_request?
super || valid_api_key? || valid_jwt_token?
end
def valid_api_key?
request.headers['X-API-Key'] == current_user&.api_key
end
def valid_jwt_token?
# JWT verification logic
token = request.headers['Authorization']&.sub(/^Bearer /, '')
decoded = JWT.decode(token, secret_key) rescue nil
decoded.present?
end
end
Per-Form Tokens
Rails supports per-form tokens for additional security. Each form receives a unique token, preventing token reuse across different forms. Enable this feature in configuration.
# config/application.rb
config.action_controller.per_form_csrf_tokens = true
# Now each form gets unique token
<%= form_with model: @user %> # Token specific to users form
<%= form_with model: @post %> # Different token for posts form
Origin Checking
Rails validates the Origin header when present, providing defense in depth. Configure allowed origins for cross-origin requests if needed.
# config/application.rb
config.action_controller.forgery_protection_origin_check = true
# Only accept requests from these origins
# Useful for mobile apps or specific subdomains
Rails.application.config.hosts << "mobile.example.com"
Practical Examples
Basic Form Protection
A user profile update form demonstrates standard CSRF protection. Rails automatically includes the token, and the server validates it before processing the request.
# Controller
class ProfilesController < ApplicationController
before_action :authenticate_user!
def edit
@user = current_user
end
def update
@user = current_user
# CSRF token validated automatically before this executes
if @user.update(profile_params)
redirect_to profile_path, notice: 'Profile updated'
else
render :edit
end
end
private
def profile_params
params.require(:user).permit(:name, :email, :bio)
end
end
# View (edit.html.erb)
<%= form_with model: @user, url: profile_path, method: :patch do |f| %>
<%= f.label :name %>
<%= f.text_field :name %>
<%= f.label :email %>
<%= f.email_field :email %>
<%= f.label :bio %>
<%= f.text_area :bio %>
<%= f.submit 'Update Profile' %>
<% end %>
# Without CSRF protection, attacker could:
# <form action="https://app.com/profile" method="POST">
# <input name="user[email]" value="attacker@evil.com">
# </form>
# <script>document.forms[0].submit()</script>
# With protection, this fails due to missing/invalid token
AJAX Request Protection
Single-page applications make extensive AJAX requests requiring CSRF tokens. Rails UJS handles this automatically, but custom JavaScript needs explicit token inclusion.
# Controller action
class TasksController < ApplicationController
def create
@task = current_user.tasks.build(task_params)
if @task.save
render json: @task, status: :created
else
render json: @task.errors, status: :unprocessable_entity
end
end
private
def task_params
params.require(:task).permit(:title, :description)
end
end
# JavaScript (using Fetch API)
class TaskManager {
constructor() {
this.csrfToken = document.querySelector('meta[name="csrf-token"]').content
}
async createTask(taskData) {
const response = await fetch('/tasks', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': this.csrfToken
},
body: JSON.stringify({ task: taskData })
})
if (!response.ok) {
throw new Error('Failed to create task')
}
return response.json()
}
}
// Usage
const manager = new TaskManager()
manager.createTask({
title: 'Complete documentation',
description: 'Write CSRF guide'
})
API Endpoint with Token Authentication
APIs using JWT or OAuth tokens instead of session cookies avoid CSRF vulnerabilities. The token must be explicitly included in the Authorization header, which attackers cannot access cross-origin.
class Api::V1::BaseController < ActionController::API
before_action :authenticate_user!
private
def authenticate_user!
token = request.headers['Authorization']&.sub(/^Bearer /, '')
begin
payload = JWT.decode(token, Rails.application.secret_key_base, true, { algorithm: 'HS256' })
@current_user = User.find(payload[0]['user_id'])
rescue JWT::DecodeError, ActiveRecord::RecordNotFound
render json: { error: 'Unauthorized' }, status: :unauthorized
end
end
def current_user
@current_user
end
end
class Api::V1::PostsController < Api::V1::BaseController
def create
@post = current_user.posts.build(post_params)
if @post.save
render json: @post, status: :created
else
render json: { errors: @post.errors }, status: :unprocessable_entity
end
end
private
def post_params
params.require(:post).permit(:title, :content)
end
end
# Client request
# fetch('/api/v1/posts', {
# method: 'POST',
# headers: {
# 'Authorization': 'Bearer eyJhbGc...',
# 'Content-Type': 'application/json'
# },
# body: JSON.stringify({ post: { title: 'New Post', content: '...' } })
# })
# CSRF-safe: attacker cannot access JWT from cross-origin
Financial Transaction Protection
High-value operations like fund transfers require additional verification beyond standard CSRF tokens. Implement re-authentication or confirmation tokens for critical actions.
class TransfersController < ApplicationController
before_action :authenticate_user!
before_action :require_recent_authentication, only: [:create]
def new
@transfer = Transfer.new
@accounts = current_user.accounts
end
def create
@transfer = current_user.transfers.build(transfer_params)
# Verify confirmation token
unless valid_confirmation_token?
flash[:error] = 'Invalid confirmation token'
render :new
return
end
if @transfer.save
TransferMailer.confirmation_email(@transfer).deliver_later
redirect_to transfers_path, notice: 'Transfer completed'
else
render :new
end
end
private
def transfer_params
params.require(:transfer).permit(:from_account_id, :to_account_id, :amount)
end
def require_recent_authentication
if session[:authenticated_at] < 5.minutes.ago
redirect_to reauthenticate_path, alert: 'Please re-authenticate'
end
end
def valid_confirmation_token?
expected = session[:transfer_confirmation_token]
actual = params[:confirmation_token]
session.delete(:transfer_confirmation_token)
expected.present? && ActiveSupport::SecurityUtils.secure_compare(expected, actual)
end
end
# View includes confirmation token
# Generated when form loads and validated on submit
<%= form_with model: @transfer do |f| %>
<%= hidden_field_tag :confirmation_token, generate_confirmation_token %>
<%= f.collection_select :from_account_id, @accounts, :id, :name %>
<%= f.collection_select :to_account_id, @accounts, :id, :name %>
<%= f.number_field :amount %>
<%= f.submit 'Transfer Funds' %>
<% end %>
# Helper method
def generate_confirmation_token
token = SecureRandom.hex(32)
session[:transfer_confirmation_token] = token
token
end
Common Patterns
Synchronizer Token Pattern
The synchronizer token pattern generates a unique, unpredictable token per session and includes it in forms or request headers. The server validates the token on each state-changing request. Rails implements this pattern by default.
# Token generation (simplified Rails implementation)
class CsrfProtection
def initialize(app)
@app = app
end
def call(env)
request = ActionDispatch::Request.new(env)
# Generate token if needed
if request.session[:csrf_token].nil?
request.session[:csrf_token] = SecureRandom.base64(32)
end
# Validate on state-changing requests
if state_changing_request?(request)
unless valid_token?(request)
return [403, {}, ['CSRF token validation failed']]
end
end
@app.call(env)
end
private
def state_changing_request?(request)
!request.get? && !request.head? && !request.options?
end
def valid_token?(request)
token = request.headers['X-CSRF-Token'] ||
request.params['authenticity_token']
token.present? &&
ActiveSupport::SecurityUtils.secure_compare(
token,
request.session[:csrf_token]
)
end
end
Double-Submit Cookie Pattern
The double-submit cookie pattern stores the CSRF token in a cookie and requires clients to submit it separately in a header or form field. The server validates that both values match. This pattern works without server-side state.
class DoubleSubmitCsrfProtection
def initialize(app)
@app = app
end
def call(env)
request = ActionDispatch::Request.new(env)
# Set CSRF cookie if missing
unless request.cookies['csrf_token']
token = SecureRandom.base64(32)
response = @app.call(env)
Rack::Utils.set_cookie_header!(
response[1],
'csrf_token',
{
value: token,
httponly: false, # JavaScript needs to read it
secure: true,
same_site: :strict
}
)
return response
end
# Validate on state-changing requests
if state_changing_request?(request)
cookie_token = request.cookies['csrf_token']
header_token = request.headers['X-CSRF-Token']
unless cookie_token && header_token &&
ActiveSupport::SecurityUtils.secure_compare(cookie_token, header_token)
return [403, {}, ['CSRF validation failed']]
end
end
@app.call(env)
end
private
def state_changing_request?(request)
!request.get? && !request.head?
end
end
# Client-side JavaScript
const csrfToken = document.cookie
.split('; ')
.find(row => row.startsWith('csrf_token='))
?.split('=')[1]
fetch('/api/data', {
method: 'POST',
headers: {
'X-CSRF-Token': csrfToken,
'Content-Type': 'application/json'
},
body: JSON.stringify({ data: 'value' })
})
SameSite Cookie Attribute
The SameSite cookie attribute prevents browsers from sending cookies with cross-site requests. Setting SameSite to Strict or Lax provides CSRF protection without requiring tokens for many scenarios.
# config/initializers/session_store.rb
Rails.application.config.session_store :cookie_store,
key: '_app_session',
same_site: :lax, # or :strict
secure: Rails.env.production?, # HTTPS only in production
httponly: true
# :strict - Never send cookie cross-site
# Most secure but breaks some legitimate flows
# :lax - Send cookie on safe cross-site requests (GET)
# Balances security and usability
# :none - Always send cookie (requires secure: true)
# Use only when necessary for cross-site functionality
Custom Request Header Validation
Custom headers require JavaScript and cannot be set by simple HTML forms. Requiring a custom header on state-changing requests prevents CSRF from basic attacks. Combine with token validation for defense in depth.
class CustomHeaderProtection < ApplicationController
before_action :verify_custom_header, except: [:index, :show]
private
def verify_custom_header
unless request.headers['X-Requested-With'] == 'XMLHttpRequest' ||
request.headers['X-Custom-Header'] == 'AppClient'
head :forbidden
end
end
end
# Client includes custom header
fetch('/api/resource', {
method: 'POST',
headers: {
'X-Custom-Header': 'AppClient',
'Content-Type': 'application/json'
},
body: JSON.stringify({ data: 'value' })
})
# Attacker cannot set custom headers cross-origin
# Browser preflight request will fail
Origin and Referer Validation
Validating the Origin or Referer headers confirms requests originate from the expected domain. This provides additional protection but should not be the sole defense, as proxies or browser extensions may strip these headers.
class OriginValidation < ApplicationController
before_action :validate_origin, unless: -> { request.get? }
private
def validate_origin
origin = request.headers['Origin'] || request.headers['Referer']
unless valid_origin?(origin)
Rails.logger.warn("Invalid origin: #{origin} for #{request.path}")
head :forbidden
end
end
def valid_origin?(origin)
return false if origin.blank?
uri = URI.parse(origin)
allowed_hosts = [
request.host,
'mobile.example.com',
'app.example.com'
]
allowed_hosts.include?(uri.host)
rescue URI::InvalidURIError
false
end
end
Common Pitfalls
GET Requests for State Changes
Applications that use GET requests for state-changing operations face heightened CSRF risk. Attackers can trigger GET requests through images, links, or scripts without requiring forms or JavaScript.
# Vulnerable pattern
get '/delete_account' do
current_user.destroy
redirect_to root_path
end
# Exploited with simple image tag
# <img src="https://app.com/delete_account">
# Correct approach
delete '/account' do
# Protected by CSRF token
current_user.destroy
redirect_to root_path
end
Disabling CSRF Protection Globally
Disabling CSRF protection across the entire application to fix specific issues creates widespread vulnerabilities. Disable protection selectively only for specific actions that genuinely do not require it.
# Dangerous - disables protection everywhere
class ApplicationController < ActionController::Base
skip_before_action :verify_authenticity_token
end
# Better - disable only where necessary
class WebhooksController < ApplicationController
skip_before_action :verify_authenticity_token, only: [:github_webhook]
before_action :verify_github_signature, only: [:github_webhook]
def github_webhook
# Webhook verified through signature instead of CSRF token
end
end
Ignoring AJAX Request Protection
Single-page applications must include CSRF tokens in AJAX requests. Forgetting to add the token header causes validation failures or creates vulnerabilities if protection is disabled.
# Vulnerable - no CSRF token included
fetch('/api/posts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title: 'New Post' })
})
// Request fails CSRF validation
# Correct - include CSRF token
const token = document.querySelector('meta[name="csrf-token"]').content
fetch('/api/posts', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': token
},
body: JSON.stringify({ title: 'New Post' })
})
Token Leakage in URLs
Embedding CSRF tokens in URLs exposes them through browser history, server logs, and Referer headers. Tokens belong in form fields or request headers, never in URLs.
# Vulnerable - token in URL
link_to 'Delete', post_path(@post, authenticity_token: form_authenticity_token),
method: :delete
# URL: /posts/1?authenticity_token=abc123
# Token leaked in logs and browser history
# Correct - use data attributes for UJS
link_to 'Delete', post_path(@post), method: :delete, data: { confirm: 'Sure?' }
# UJS reads token from meta tag and includes in request
Subdomain Cookie Scope
Setting cookies with overly broad domain scope allows subdomains to access the session cookie. A compromised subdomain could read session cookies and craft malicious requests.
# Vulnerable - cookie accessible to all subdomains
Rails.application.config.session_store :cookie_store,
key: '_app_session',
domain: '.example.com' # All subdomains can read
# Safer - limit to specific host
Rails.application.config.session_store :cookie_store,
key: '_app_session',
domain: 'app.example.com' # Only this host
Mixing Authentication Methods
Applications supporting both cookie-based and token-based authentication must ensure CSRF protection applies appropriately to each. Cookie-authenticated requests require CSRF tokens; token-authenticated requests do not.
class ApplicationController < ActionController::Base
protect_from_forgery with: :exception, unless: :api_request?
private
def api_request?
# Skip CSRF for JWT authentication
request.headers['Authorization'].present?
end
# Problem: attacker could send Authorization header
# with invalid token to bypass CSRF check
end
# Better approach
class ApplicationController < ActionController::Base
protect_from_forgery with: :exception
private
def verified_request?
super || valid_api_authentication?
end
def valid_api_authentication?
token = request.headers['Authorization']&.sub(/^Bearer /, '')
return false if token.blank?
JWT.decode(token, secret_key) rescue false
end
end
Caching Pages with Tokens
Caching pages that include CSRF tokens can serve stale tokens to users. The cached token may not match the user's session, causing validation failures. Exclude token fields from cache or generate tokens dynamically.
# Problem: cached form has outdated token
class PostsController < ApplicationController
caches_page :new
def new
@post = Post.new
end
end
# Solution: disable caching for pages with forms
class PostsController < ApplicationController
def new
@post = Post.new
expires_now # Prevent caching
end
end
# Or use JavaScript to inject fresh token
# <form id="post-form">
# <!-- static form fields -->
# </form>
# <script>
# const form = document.getElementById('post-form')
# const token = document.querySelector('meta[name="csrf-token"]').content
# const input = document.createElement('input')
# input.type = 'hidden'
# input.name = 'authenticity_token'
# input.value = token
# form.appendChild(input)
# </script>
Reference
CSRF Protection Methods
| Method | Description | Use Case |
|---|---|---|
| Synchronizer Token | Server generates unique token per session, validates on requests | Default Rails protection, stateful applications |
| Double-Submit Cookie | Token stored in cookie, submitted in header for comparison | Stateless APIs, microservices |
| SameSite Cookies | Browser restricts cookie sending to same-site requests | Modern browsers, primary domain only |
| Custom Headers | Require custom header that simple forms cannot set | API endpoints, JavaScript-heavy apps |
| Origin Validation | Verify Origin/Referer header matches expected domain | Defense in depth, supplement to token validation |
Rails Protection Strategies
| Strategy | Behavior | When to Use |
|---|---|---|
| :exception | Raise ActionController::InvalidAuthenticityToken | Development, debugging |
| :null_session | Clear session on validation failure | Production, continue with empty session |
| :reset_session | Reset entire session on validation failure | High-security applications |
HTTP Methods and CSRF Risk
| Method | Should Modify State | CSRF Risk | Protection Required |
|---|---|---|---|
| GET | No | Low (if used correctly) | No, but validate sensitive queries |
| POST | Yes | High | Yes, CSRF token required |
| PUT | Yes | High | Yes, CSRF token required |
| PATCH | Yes | High | Yes, CSRF token required |
| DELETE | Yes | High | Yes, CSRF token required |
| HEAD | No | None | No |
| OPTIONS | No | None | No |
SameSite Cookie Values
| Value | Cross-Site Sending | Protection Level | Compatibility |
|---|---|---|---|
| Strict | Never | Highest | May break legitimate flows |
| Lax | Only safe methods (GET) | High | Good balance |
| None | Always (requires Secure) | Low | Required for cross-site cookies |
Rails Configuration Options
| Option | Default | Purpose |
|---|---|---|
| per_form_csrf_tokens | false | Generate unique token per form |
| forgery_protection_origin_check | false | Validate Origin header |
| allow_forgery_protection | true | Enable/disable CSRF protection |
| default_protect_from_forgery | false | Protect all controllers by default |
Token Validation Headers
| Header | Purpose | Required |
|---|---|---|
| X-CSRF-Token | Submit CSRF token in AJAX requests | For state-changing AJAX |
| X-Requested-With | Indicate AJAX request | Optional, additional validation |
| Authorization | JWT or OAuth token | Alternative to CSRF for APIs |
| Origin | Request origin domain | Automatic, used for validation |
| Referer | Previous page URL | Automatic, fallback for validation |
Common Rails Helper Methods
| Method | Purpose | Example |
|---|---|---|
| form_authenticity_token | Get current CSRF token | form_authenticity_token |
| csrf_meta_tags | Render meta tags with token | <%= csrf_meta_tags %> |
| protect_from_forgery | Enable CSRF protection | protect_from_forgery with: :exception |
| skip_before_action | Disable protection for action | skip_before_action :verify_authenticity_token |
| verify_authenticity_token | Manually trigger validation | verify_authenticity_token |
| verified_request? | Check if request passes validation | verified_request? |
Attack Indicators
| Indicator | Possible CSRF Attack | Investigation Action |
|---|---|---|
| Missing/invalid CSRF token | Failed CSRF attempt | Log IP, user agent, monitor for patterns |
| Origin header mismatch | Cross-origin request | Verify legitimate cross-origin needs |
| Unusual request timing | Automated attack | Check for rapid requests, same source |
| Unexpected state changes | Successful CSRF | Audit recent changes, check logs |
| User reports unauthorized actions | Successful attack | Review user's recent activity, reset session |