Picture a bank check: it's not enough for the check to look legitimate — the bank also verifies it was actually issued by the account holder, not forged by someone who copied the layout. Drupal forms work the same way. Anyone can view your form's HTML and craft a fake POST request that looks identical — that's a Cross-Site Request Forgery (CSRF) attack, where a malicious site tricks a logged-in visitor's browser into silently submitting a request on your site. The defense is a form token: a unique, session-tied value baked into every form that proves the submission really came from your own rendered form, not a forged copy.
What you'll learn in this lesson
- What a CSRF attack actually is, and why forged form submissions are dangerous even against a logged-in, trusted user
- Why you almost never write CSRF protection code yourself — the Form API handles it automatically
- How to validate a token manually, for the rare case of a submission path that bypasses the Form API entirely
The Form API already protects you
Every form built through FormBase/buildForm() gets a hidden form_token field automatically — you never add it yourself:
namespace Drupal\my_module\Form;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
class FeedbackForm extends FormBase {
public function getFormId() {
return 'feedback_form';
}
public function buildForm(array $form, FormStateInterface $form_state) {
$form['email'] = [
'#type' => 'email',
'#title' => $this->t('Your Email'),
'#required' => TRUE,
];
$form['feedback'] = [
'#type' => 'textarea',
'#title' => $this->t('Your Feedback'),
];
$form['submit'] = [
'#type' => 'submit',
'#value' => $this->t('Submit'),
];
return $form;
}
public function submitForm(array &$form, FormStateInterface $form_state) {
$this->messenger()->addMessage($this->t('Thanks for your feedback, we will review your input.'));
}
}
Nothing in buildForm() mentions a token — Drupal's FormBuilder service injects it during rendering and checks it during submission, entirely behind the scenes. View the rendered page's HTML source and you'll find a hidden <input name="form_token"> tied to the visitor's session; submit without a matching token and Drupal rejects the request before your submitForm() even runs.
$this->messenger()->addMessage(), not drupal_set_message(). That procedural function was removed entirely in Drupal 8 — the modern replacement is the Messenger service, available via $this->messenger() in any class using MessengerTrait (which FormBase already includes).When you'd validate a token by hand
The one case where automatic protection doesn't apply is a submission path that bypasses the Form API entirely — a raw AJAX endpoint or custom controller accepting POST data directly. There, you generate and check the token yourself via the csrf_token service:
use Drupal\Core\Form\FormStateInterface;
/**
* Validates a token submitted outside the normal Form API flow.
*/
function validate_custom_submission($form, FormStateInterface $form_state) {
$token = $form_state->getValue('form_token');
if (!\Drupal::service('csrf_token')->validate($token, 'your_custom_action')) {
\Drupal::messenger()->addMessage(t('Invalid token, please try again.'), 'error');
}
}
The second argument to validate() — here 'your_custom_action' — is a value string that must match whatever value the token was generated against; this scopes a token to one specific action, so a token minted for one form can't be replayed against a different one.
Quick check: you're building a REST resource that accepts POST requests from an external mobile app, authenticated via an API key rather than a browser session. Does it need Drupal's form-token CSRF protection? (Answer: no — CSRF specifically exploits a browser silently reusing a logged-in session's cookies. An API-key-authenticated request from a mobile app isn't riding on a browser session cookie, so the threat model is different; that endpoint needs its own authentication/authorization checks, not form tokens.)
Key takeaways
- A CSRF attack tricks a logged-in visitor's browser into submitting a forged request to your site without their knowledge — form tokens defeat this by proving a submission actually came from your rendered form.
- Every form built via the Form API gets automatic token generation and validation — you don't write this code yourself for standard forms.
- Only hand-roll token validation for submission paths that bypass the Form API (raw AJAX endpoints, custom controllers reading POST directly), using the
csrf_tokenservice. drupal_set_message()is dead Drupal 7 code — use$this->messenger()->addMessage()(inside a class usingMessengerTrait) or\Drupal::messenger()->addMessage()in procedural code instead.- CSRF protection and general request authentication (like API keys) solve different problems — don't assume one covers the other.
Coming up next
Tokens confirm a submission came from the right place — but they say nothing about whether the data inside that submission is safe to store or display. Next, we'll look at sanitizing form input itself: how Drupal's render system automatically escapes output, and when you still need to reach for Xss::filter() yourself.