Ein einziger fehlender CSRF-Token kann reichen, damit ein Angreifer im Namen eines eingeloggten Benutzers Aktionen ausloest. 2024 wurde eine bekannte E-Commerce-Plattform kompromittiert, weil ein Entwickler skip_before_action :verify_authenticity_token in einem Admin-Controller hinterlassen hatte, der eigentlich nur fuer einen temporaeren Webhook gedacht war. Der Angriff: ein praepariertes Bild-Tag in einer E-Mail, das einen POST-Request auf den ungeschuetzten Endpoint ausloeste.
Rails bringt soliden CSRF-Schutz mit. Das Problem liegt selten am Framework, sondern an der Konfiguration. Dieser Artikel zeigt, wo die typischen Fehler passieren und wie man sie vermeidet.
Rails Cross-Site Request Forgery (CSRF)

Wie CSRF-Angriffe in Rails funktionieren
Bevor wir in die Konfiguration einsteigen: Was passiert bei einem CSRF-Angriff konkret?
- Ein Benutzer ist in deiner Rails-App eingeloggt (Session-Cookie aktiv)
- Er besucht eine manipulierte Website oder oeffnet eine praeparierte E-Mail
- Ein verstecktes Formular oder Bild-Tag loest einen Request an deine App aus
- Der Browser sendet automatisch den Session-Cookie mit
- Deine App fuehrt die Aktion aus, weil die Session gueltig ist
Der Angriff funktioniert, weil der Browser Cookies automatisch an die Ziel-Domain sendet. Ohne Token-Validierung kann der Server nicht unterscheiden, ob der Request vom eigenen Frontend oder von einer fremden Seite kommt.
Ein realistisches Angriffsszenario:
<!-- Auf einer Angreifer-Website -->
<img src="https://deine-app.com/admin/users/42/promote?role=admin" />
<!-- Oder per verstecktem Formular -->
<form action="https://deine-app.com/transfers" method="POST">
<input type="hidden" name="amount" value="10000" />
<input type="hidden" name="to_account" value="ATTACKER-IBAN" />
</form>
<script>document.forms[0].submit();</script>
protect_from_forgery: Die drei Strategien im Detail
Die Methode protect_from_forgery im ApplicationController ist die erste Verteidigungslinie:
class ApplicationController < ActionController::Base
protect_from_forgery with: :exception
end
Rails bietet drei Strategien, die sich grundlegend unterscheiden:
| Strategie | Verhalten bei ungueltigem Token | Seiteneffekt |
|---|---|---|
:exception | Wirft ActionController::InvalidAuthenticityToken | Request wird komplett abgebrochen, Fehler wird geloggt |
:null_session | Setzt request.session auf ein leeres Hash | Request wird ausgefuehrt, aber ohne Session-Daten |
:reset_session | Loescht die gesamte Session | Benutzer wird ausgeloggt, Request wird ausgefuehrt |
Der kritische Unterschied, den viele uebersehen
Bei :null_session und :reset_session wird der Request trotzdem ausgefuehrt. Nur die Session-Daten sind weg. Wenn dein Controller eine Aktion hat, die nicht auf Session-Daten angewiesen ist (z.B. ein oeffentliches Formular), wird diese Aktion auch ohne gueltigen Token ausgefuehrt.
# Gefaehrlich mit :null_session
class TransfersController < ApplicationController
def create
# current_user ist nil wegen null_session
# Aber wenn die Aktion keinen current_user braucht...
Transfer.create!(params[:transfer]) # wird trotzdem ausgefuehrt!
end
end
Empfehlung: Verwende :exception als Standard. Du willst, dass ungueltige Requests sofort fehlschlagen.
USEO’s Take
In ueber 50 Rails-Projekten, die wir gewartet oder modernisiert haben, war :null_session die haeufigste Fehlkonfiguration. Teams setzen es ein, weil sie denken, es sei “weniger aggressiv”. In Wirklichkeit verschleiert es Sicherheitsprobleme: Der Request schlaegt leise fehl, niemand bemerkt den fehlenden Token, und im schlimmsten Fall werden Aktionen ohne Session-Kontext ausgefuehrt. Nutze :exception und baue einen Error-Handler, der ActionController::InvalidAuthenticityToken sauber abfaengt.
Unterschiede zwischen Rails 5, 6 und 7
Die CSRF-Implementierung hat sich ueber die Versionen weiterentwickelt:
Rails 5.2: Per-Form CSRF Tokens
Rails 5.2 fuehrte per-form CSRF Tokens ein. Statt eines einzigen Tokens pro Session generiert Rails fuer jedes Formular einen eigenen Token, der an die Ziel-Action gebunden ist:
# config/application.rb
config.action_controller.per_form_csrf_tokens = true
Das verhindert, dass ein Token aus Formular A fuer Formular B missbraucht wird. Seit Rails 5.2 ist diese Option standardmaessig aktiviert.
Rails 6: Origin-Pruefung
Ab Rails 6 prueft protect_from_forgery zusaetzlich den Origin-Header:
# config/application.rb
config.action_controller.forgery_protection_origin_check = true
Wenn der Origin-Header nicht mit dem Host der App uebereinstimmt, wird der Request abgelehnt, selbst wenn der Token korrekt ist. Das ist eine zusaetzliche Verteidigungsschicht.
Rails 7: Verschluesselte Cookies als Standard
Rails 7 setzt standardmaessig auf verschluesselte Cookies statt signed Cookies:
# Rails 7 Standard
Rails.application.config.action_dispatch.cookies_serializer = :json
Rails.application.config.session_store :cookie_store,
key: '_app_session',
same_site: :lax,
secure: Rails.env.production?
Zusaetzlich verwendet Rails 7 SameSite: Lax als Standard fuer alle Cookies, was CSRF-Angriffe ueber Cross-Site-Requests deutlich erschwert.
csrf_meta_tags: Warum sie fehlen koennen
Der csrf_meta_tags Helper im Layout generiert die Meta-Tags, die JavaScript braucht:
<!-- app/views/layouts/application.html.erb -->
<head>
<%= csrf_meta_tags %>
</head>
Das erzeugt:
<meta name="csrf-param" content="authenticity_token" />
<meta name="csrf-token" content="dein_token_wert" />
Haeufige Fehlerquelle: Bei Turbo Drive (Rails 7) oder SPA-Setups werden Seiten oft per AJAX nachgeladen. Dabei werden die Meta-Tags im <head> nicht aktualisiert. Das fuehrt zu abgelaufenen Tokens.
Loesung fuer Turbo:
// Turbo aktualisiert csrf-token automatisch bei Turbo-Visits
// Aber bei manuellen fetch-Requests brauchst du:
function getCsrfToken() {
return document.querySelector('meta[name="csrf-token"]')?.content;
}
// Nach einem Turbo-Visit wird der Token im Meta-Tag aktualisiert
document.addEventListener('turbo:load', () => {
// Token ist jetzt aktuell
});
Formulare ohne Rails-Helper: Das vergessene Token
Rails-Formulare mit form_with fuegen den Token automatisch ein:
<%= form_with model: @project do |form| %>
<%= form.label :name %>
<%= form.text_field :name %>
<%= form.submit "Speichern" %>
<% end %>
Wenn du HTML-Formulare ohne Rails-Helper erstellst, fehlt der Token:
<!-- FALSCH: Kein CSRF-Token -->
<form action="/projects" method="post">
<input type="text" name="project[name]" />
<button type="submit">Speichern</button>
</form>
<!-- RICHTIG: Token manuell einfuegen -->
<form action="/projects" method="post">
<input type="hidden" name="authenticity_token"
value="<%= form_authenticity_token %>" />
<input type="text" name="project[name]" />
<button type="submit">Speichern</button>
</form>
Das passiert haeufig bei:
- Formularen in Markdown/CMS-Content
- Eingebetteten Widgets von Drittanbietern
- Legacy-Views, die vor der Rails-Migration erstellt wurden
Warum GET-Requests fuer Zustandsaenderungen gefaehrlich sind
Rails prueft CSRF-Tokens nur bei POST, PUT, PATCH und DELETE. GET-Requests werden nicht geprueft, weil sie laut HTTP-Spezifikation idempotent sein sollen.
Das wird zum Problem, wenn Entwickler GET-Requests fuer Zustandsaenderungen verwenden:
# GEFAEHRLICH: GET-Route fuer Zustandsaenderung
get '/users/:id/deactivate', to: 'users#deactivate'
# In einer E-Mail oder fremden Website:
# <img src="https://deine-app.com/users/42/deactivate" />
# -> Benutzer 42 wird deaktiviert, ohne CSRF-Schutz
| HTTP-Methode | CSRF-Token geprueft? | Fuer Zustandsaenderungen geeignet? |
|---|---|---|
| GET | Nein | Nein |
| POST | Ja | Ja |
| PUT/PATCH | Ja | Ja |
| DELETE | Ja | Ja |
Regel: Jede Aktion, die Daten veraendert, muss hinter einem nicht-GET HTTP-Verb stehen.
skip_forgery_protection: Wann es legitim ist (und wann nicht)
Suche in deinem Code nach:
grep -rn "skip_forgery_protection\|skip_before_action.*verify_authenticity_token" app/controllers/
Legitime Faelle
- API-only Controller mit Token-Authentifizierung
class Api::V1::BaseController < ActionController::API
# ActionController::API hat keinen CSRF-Schutz
# Das ist korrekt, weil API-Clients keine Browser-Cookies verwenden
before_action :authenticate_api_token!
end
- Webhook-Endpoints mit Signatur-Verifizierung
class WebhooksController < ApplicationController
skip_forgery_protection only: [:stripe, :github]
before_action :verify_webhook_signature
private
def verify_webhook_signature
# Stripe/GitHub senden eine Signatur im Header
# Die wird gegen das Shared Secret verifiziert
payload = request.body.read
sig = request.headers['Stripe-Signature']
Stripe::Webhook::Signature.verify_header(payload, sig, endpoint_secret)
end
end
- Health-Check Endpoints
class HealthController < ApplicationController
skip_forgery_protection only: [:show]
def show
render json: { status: 'ok' } # Keine Zustandsaenderung, nur Lesezugriff
end
end
Nicht legitime Faelle
- “Es funktioniert nicht mit meinem JavaScript” (dann fehlt das Token im Request)
- “Wir brauchen es nicht, wir haben ja ein Login” (Login schuetzt nicht vor CSRF)
- “Es ist nur ein internes Tool” (interne Tools sind genauso angreifbar)
USEO’s Take
Wir sehen in Code-Audits regelmaessig skip_before_action :verify_authenticity_token im ApplicationController oder in Base-Controllern, weil ein Entwickler irgendwann ein AJAX-Problem damit “geloest” hat. Wenn du CSRF-Schutz global deaktivierst, hast du keinen CSRF-Schutz. Klingt offensichtlich, passiert aber erstaunlich oft. Unser Vorgehen: Wir behandeln jedes skip_forgery_protection wie ein TODO und dokumentieren den Grund direkt im Code.
AJAX und SPA: Token-Handling ohne Rails-Formular
Bei modernen JavaScript-Frontends musst du den CSRF-Token manuell mitschicken:
Vanilla JavaScript / Fetch API
const token = document.querySelector('meta[name="csrf-token"]').content;
fetch('/posts', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': token
},
body: JSON.stringify({ post: { title: 'Neuer Beitrag' } }),
credentials: 'same-origin' // Wichtig: Cookies mitsenden
});
Axios (React, Vue)
import axios from 'axios';
// Einmal konfigurieren
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content;
axios.defaults.headers.common['X-CSRF-Token'] = csrfToken;
axios.defaults.withCredentials = true;
// Danach automatisch in jedem Request
axios.post('/posts', { post: { title: 'Neuer Beitrag' } });
Problem: Token-Rotation bei langen Sessions
Wenn ein Benutzer eine SPA stundenlang offen hat, kann der Token in den Meta-Tags veralten. Rails rotiert Tokens bei jedem Request. Loesungen:
# Option 1: Token per API-Endpoint bereitstellen
class CsrfTokensController < ApplicationController
def show
render json: { token: form_authenticity_token }
end
end
// Option 2: Token aus Response-Header lesen
// Rails setzt den neuen Token im X-CSRF-Token Response-Header
fetch('/posts', { method: 'POST', /* ... */ })
.then(response => {
const newToken = response.headers.get('X-CSRF-Token');
if (newToken) {
document.querySelector('meta[name="csrf-token"]').content = newToken;
}
});
Cookie-Konfiguration: SameSite, Secure, HttpOnly
Die richtige Cookie-Konfiguration ist eine zusaetzliche Schutzschicht:
# config/initializers/session_store.rb
Rails.application.config.session_store :cookie_store,
key: '_app_session',
same_site: :lax,
secure: Rails.env.production?,
httponly: true
Was jedes Attribut bewirkt:
| Attribut | Wirkung | Empfehlung |
|---|---|---|
same_site: :lax | Cookie wird bei Cross-Site-Requests nur bei Top-Level-Navigation gesendet | Standard fuer die meisten Apps |
same_site: :strict | Cookie wird nie bei Cross-Site-Requests gesendet | Fuer Hochsicherheits-Apps (Banking, Gesundheitswesen) |
secure: true | Cookie nur ueber HTTPS | Pflicht in Produktion |
httponly: true | Cookie nicht per JavaScript lesbar | Immer aktivieren |
Achtung: SameSite: Strict kann dazu fuehren, dass Benutzer nach dem Klick auf einen Link (z.B. aus einer E-Mail) nicht eingeloggt sind, weil der Cookie nicht mitgesendet wird. Fuer die meisten Anwendungen ist :lax der bessere Kompromiss.
XSS als CSRF-Verstaerker
Eine XSS-Schwachstelle macht jeden CSRF-Schutz wirkungslos. Wenn ein Angreifer JavaScript in deiner App ausfuehren kann, liest er den Token einfach aus:
// Ein Angreifer mit XSS-Zugang:
const token = document.querySelector('meta[name="csrf-token"]').content;
// Jetzt kann er beliebige Requests mit gueltigem Token senden
fetch('/admin/users', {
method: 'DELETE',
headers: { 'X-CSRF-Token': token },
credentials: 'same-origin'
});
Rails schuetzt mit automatischem HTML-Escaping in ERB-Templates. Kritische Stellen, an denen XSS trotzdem entstehen kann:
raw()oderhtml_safeauf Benutzereingabenrender inline:mit unkontrollierten Parametern- JavaScript-Templates, die Daten unescaped einfuegen
- Content Security Policy (CSP) fehlt oder ist zu permissiv
# GEFAEHRLICH
<%= raw @user.bio %>
<%= @comment.body.html_safe %>
# SICHER
<%= @user.bio %>
<%= sanitize @comment.body %>
Implementiere CSP-Header als zusaetzliche Schicht:
# config/initializers/content_security_policy.rb
Rails.application.configure do
config.content_security_policy do |policy|
policy.default_src :self
policy.script_src :self
policy.style_src :self, :unsafe_inline
end
end
CSRF in API-only Apps: Wann du ihn wirklich deaktivieren kannst
Wenn deine Rails-App ausschliesslich als API dient (kein Browser-Frontend, keine Cookies), ist CSRF-Schutz nicht noetig. Der Grund: CSRF-Angriffe funktionieren ueber automatisch gesendete Cookies. Ohne Cookies kein CSRF.
# API-only Application
class ApplicationController < ActionController::API
# ActionController::API hat keinen CSRF-Schutz
# Authentifizierung laeuft ueber Bearer Token / API Key
end
USEO’s Take
Vorsicht bei “hybriden” Apps. Wir hatten einen Fall, wo eine Rails-App als API-only gestartet wurde, dann aber ein Admin-Panel mit Session-Cookies dazukam. Die API-Controller erbten von ActionController::API, die Admin-Controller von ActionController::Base, aber einige Shared-Controller waren falsch zugeordnet. Ergebnis: Admin-Aktionen ohne CSRF-Schutz. Wenn du hybrid arbeitest, pruefe die Controller-Hierarchie genau.
Entscheidungsmatrix:
| Setup | CSRF noetig? | Authentifizierung |
|---|---|---|
| Klassische Web-App mit Sessions | Ja | Session-Cookie |
| SPA mit Cookie-basierter Auth | Ja | Session-Cookie via AJAX |
| API mit Bearer Token | Nein | Authorization Header |
| API mit API-Key im Header | Nein | Custom Header |
| Hybrid (Web + API) | Ja, fuer Web-Routen | Gemischt |
Token-Leaks verhindern
CSRF-Tokens gehoeren nicht in URLs. Sie koennen ueber Referer-Header, Browser-History, Server-Logs und geteilte Links exponiert werden.
Checkliste fuer Token-Sicherheit:
- Tokens nur in HTTP-Headern, Hidden Fields oder HttpOnly-Cookies uebertragen
- Keine Tokens in URL-Parametern
- Keine Tokens in localStorage oder sessionStorage (XSS-angreifbar)
- Tokens nicht in Log-Dateien schreiben (Parameter-Filter konfigurieren)
- Tokens nicht in Fehlermeldungen an den Client senden
# config/initializers/filter_parameter_logging.rb
Rails.application.config.filter_parameters += [
:authenticity_token, :password, :token
]
Checkliste
Setup
-
protect_from_forgery with: :exceptionim ApplicationController -
csrf_meta_tagsim Layout-Head - Per-form CSRF Tokens aktiviert (Rails 5.2+)
- Origin-Pruefung aktiviert (Rails 6+)
Formulare und Requests
- Alle Formulare verwenden
form_withoder fuegen Token manuell ein - Keine GET-Routes fuer Zustandsaenderungen
- AJAX-Requests senden
X-CSRF-TokenHeader -
credentials: 'same-origin'bei fetch-Requests
Ausnahmen
- Jedes
skip_forgery_protectionist dokumentiert und begruendet - API-Endpoints haben alternative Authentifizierung (Bearer Token, API Key)
- Webhook-Endpoints verifizieren Signaturen
Cookies
-
SameSite: LaxoderStrictkonfiguriert -
Secure: truein Produktion -
HttpOnly: truefuer Session-Cookies
XSS-Praevention
- Kein
html_safeoderrawauf Benutzereingaben - Content Security Policy konfiguriert
- Regelmaessige Security-Audits
Token-Sicherheit
- Tokens nie in URLs
-
authenticity_tokenin Parameter-Filter - Keine Tokens in localStorage/sessionStorage
FAQs
Wie konfiguriere ich CSRF-Schutz in einer hybriden Rails-App (Web + API)?
Trenne die Controller-Hierarchie klar:
# Web-Controller: voller CSRF-Schutz
class WebController < ActionController::Base
protect_from_forgery with: :exception
end
# API-Controller: kein CSRF, aber Token-Auth
class ApiController < ActionController::API
before_action :authenticate_api_token!
end
Stelle sicher, dass kein Controller versehentlich von der falschen Basisklasse erbt. Pruefe insbesondere Shared-Concerns und Module, die in beide Hierarchien eingebunden werden.
Was passiert, wenn ich CSRF-Schutz fuer einen Controller deaktiviere?
Der Controller akzeptiert Requests ohne gueltigen CSRF-Token. Das bedeutet: Jede Website im Internet kann Requests an diesen Controller senden, solange der Benutzer eingeloggt ist. Wenn der Controller Daten aendert, ist das ein Sicherheitsproblem. Wenn du CSRF deaktivieren musst, stelle sicher, dass eine alternative Authentifizierung vorhanden ist (z.B. API-Token im Header).
Warum reicht SameSite allein nicht als CSRF-Schutz?
SameSite: Lax blockiert nur Cross-Site-Requests, die nicht ueber Top-Level-Navigation erfolgen. Ein Angreifer kann immer noch ein Formular erstellen, das den Benutzer auf deine Seite weiterleitet (Top-Level-Navigation). Ausserdem unterstuetzen aeltere Browser SameSite nicht vollstaendig. CSRF-Tokens und SameSite-Cookies ergaenzen sich, ersetzen sich aber nicht.