Medium risk Website forms · Cross-site request forgery (CSRF)
Absence of anti-CSRF tokens: what it means and how to fix it
Short answer: a form on your site doesn't include a hidden security token. Without one, another website can secretly make a logged-in visitor's browser submit that form, for example to change their email address, password or shipping details, without them knowing.
The fix: turn on your platform's built-in CSRF protection for every form that changes something while a user is logged in, and set session cookies to SameSite=Lax. Search boxes and other forms that don't change anything can be left as they are.
What is CSRF?
Cross-site request forgery works because browsers automatically send your site's login cookie with every request to your site, wherever the request comes from. Here's how an attack plays out:
- A customer is logged in to your shop in one browser tab.
- In another tab, they visit a malicious page, perhaps from a phishing email.
- That page quietly submits a hidden form to
your-shop.com/account/emailwith the attacker's address. - The customer's browser includes their login cookie, so your site accepts the change as theirs.
- The attacker now resets the password through "forgot password" and owns the account.
An anti-CSRF token stops this. Your site puts a random secret value in each form and checks it when the form comes back. The malicious page can't read that value, so its forged request is rejected.
Is your finding a false alarm?
Scanners flag every form without a token, because they can't tell what a form does. Sort yours like this:
| Form | Needs a token? |
|---|---|
| Site search, filters, sorting | No. These don't change anything. |
| Contact or newsletter form on a site with no logins | Usually no. Use spam protection instead. |
| Login form | Recommended, to stop "login CSRF", where someone is signed in to the attacker's account. |
| Account settings, password or email change | Yes |
| Cart, checkout, orders, payments | Yes |
| Admin pages, posting content, deleting anything | Yes |
The report shows which page each flagged form is on. If they're all search or newsletter forms on a site without logins, you can note it as accepted and move on.
- 443 Absence of Anti-CSRF Tokens Found on 6 URLs (Deep Scan) Medium
- 80 Forms that may lack CSRF protection 2 search forms (Basic Scan) Info
How to fix it on common platforms
Almost every web framework has CSRF protection built in. Usually it's on by default, and the finding means a form skipped it.
WordPress. WordPress protects its own admin screens with "nonces". Flagged forms usually come from a theme or plugin. Update it, or ask its developer. In custom code, add wp_nonce_field('my_action') inside the form and check it with check_admin_referer('my_action') (or wp_verify_nonce) before acting on the submission.
Django. Keep django.middleware.csrf.CsrfViewMiddleware in MIDDLEWARE (it's on by default) and put {% csrf_token %} inside every <form method="post">. Remove any @csrf_exempt that isn't truly needed.
Laravel. Add @csrf inside each Blade form. The CSRF middleware checks it automatically for POST, PUT, PATCH and DELETE requests. Keep the list of excluded routes short.
Ruby on Rails. protect_from_forgery is on by default, and form_with adds the token for you. Hand-written forms need <%= hidden_field_tag :authenticity_token, form_authenticity_token %>.
ASP.NET Core. Form tag helpers add the token automatically. Validate it with [ValidateAntiForgeryToken] on actions, or AutoValidateAntiforgeryToken globally.
Node.js / Express. Express has no built-in CSRF protection, and the old csurf package is deprecated. Use a maintained library such as csrf-csrf or csrf-sync, or, for an API used only by your own front end, require a custom header (for example X-Requested-With) and check the request's Origin header.
Add these layers too
SameSite=Laxon session cookies. Modern browsers then don't send the login cookie with form posts from other sites. See the cookie flags guide.- Never change anything with a GET request. Links and images can trigger GET requests from any site. Actions like "delete" or "confirm order" should require POST.
- Re-ask for the password before high-risk changes such as a new email address, password or payout details.
- Check the
Originheader on state-changing requests, and reject any that come from other sites.
Test it
- Open a protected form, view the page source, and confirm a hidden field like
csrfmiddlewaretoken,_token,authenticity_tokenor_wpnonceis there. - Submit the form normally. It should still work.
- Re-run the Deep Scan. Forms that remain flagged should be the ones you decided don't need a token.
Common questions
We use SameSite cookies. Isn't that enough?
It blocks most attacks in modern browsers and is a big improvement. Tokens still cover the gaps: older browsers, GET-based actions, attacks from your own subdomains, and cookies that need SameSite=None. Use both for anything important.
Our site has no logins at all. Does this matter?
Much less. CSRF abuses a logged-in session, so without logins the main risk is spam through your forms. Use the form plugin's spam protection or a CAPTCHA.
Why did the Basic Scan flag our search box?
Nmap's check looks for forms without a token and can't tell a search box from a password form. The report lists each flagged form's name and page. If they're search forms (names often include "search"), you can safely ignore the finding.
Sources: OWASP CSRF Prevention cheat sheet · MDN: CSRF prevention · ZAP: Absence of Anti-CSRF Tokens · WordPress nonces · Django CSRF · Laravel CSRF · ASP.NET Core anti-forgery · csurf (deprecated)