Form Security and CSRF Protection
Secure web forms against cross-site request forgery, method misuse, and insecure action URLs with framework-specific implementation guides and defense-in-depth strategies.
Cross-site request forgery (CSRF) exploits the browser's automatic cookie attachment to trick authenticated users into performing actions they did not intend. An attacker hosts a page containing a hidden form that submits to your application. When a logged-in user visits that page, the browser sends their session cookie along with the forged request. Without CSRF protection, your server cannot distinguish this from a legitimate submission.
How CSRF Attacks Work
- A user logs into
bank.example.comand receives a session cookie. - The user visits
evil.example.com, which contains a hidden form:<form action="https://bank.example.com/transfer" method="POST"> <input type="hidden" name="to" value="attacker" /> <input type="hidden" name="amount" value="10000" /> </form> <script>document.forms[0].submit();</script> - The browser submits the form with the user's session cookie attached automatically.
- The server processes the transfer because the session is valid.
The attack works because HTTP cookies are sent based on the destination domain, not the originating page.
The Synchronizer Token Pattern
The most widely used CSRF defense. The server generates a unique, unpredictable token per session (or per request) and embeds it in every form as a hidden field. On submission, the server validates that the token matches the one stored in the session. An attacker cannot read the token from another origin due to the same-origin policy.
<form action="/transfer" method="POST">
<input type="hidden" name="_csrf" value="a1b2c3d4e5f6..." />
<input type="text" name="to" />
<input type="number" name="amount" />
<button type="submit">Transfer</button>
</form>
The token must be cryptographically random and tied to the user's session. Never use predictable values like timestamps or user IDs.
Double-Submit Cookie Pattern
An alternative when server-side session storage is inconvenient (e.g., stateless APIs). The server sets a random token in a cookie and also expects the same value in a request header or form field. An attacker can trigger requests that include cookies, but cannot read the cookie value to duplicate it in the header.
Set-Cookie: csrf_token=random_value; SameSite=Strict; Secure; Path=/
The client reads the cookie via JavaScript (so this cookie must not be HttpOnly — see cookie security flags for guidance on when to use each flag) and includes the value in a custom header:
X-CSRF-Token: random_value
The server compares the cookie value to the header value. This works because a cross-origin attacker cannot read cookies from your domain.
Why Login Forms Need CSRF Protection
Login forms are frequently left unprotected because developers assume there is no authenticated session to hijack. This is wrong. A login CSRF attack forces the victim to authenticate as the attacker:
- Attacker submits a hidden form that logs the victim into the attacker's account.
- The victim, now unknowingly using the attacker's session, enters sensitive data (payment info, personal details).
- The attacker logs back into their own account and retrieves the victim's data.
Always apply CSRF protection to login, registration, and password reset forms.
GET Requests Must Not Mutate State
HTML forms using method="GET" append parameters to the URL. These are trivially triggered via image tags, link prefetches, and bookmarks:
<!-- This triggers a state change via a simple image load -->
<img src="https://example.com/delete-account?confirm=true" />
Any endpoint that modifies data -- creating, updating, deleting resources -- must require POST, PUT, PATCH, or DELETE. GET requests should only retrieve data. This is not just a CSRF concern; it is a fundamental HTTP semantics requirement.
HTTPS for Form Actions
Forms with action attributes pointing to HTTP URLs transmit all submitted data in plaintext, including passwords and tokens. Even if the page itself is served over HTTPS, a form action of http://... sends the submission unencrypted.
Always use HTTPS in form actions. Better yet, use relative URLs (action="/login") so the form inherits the page's protocol. Set the Strict-Transport-Security header to ensure the browser never downgrades to HTTP — our HTTP security headers implementation guide covers HSTS and CSP configuration in detail.
Framework-Specific Implementation
Express.js (csurf / csrf-csrf)
import { doubleCsrf } from "csrf-csrf";
const { doubleCsrfProtection, generateToken } = doubleCsrf({
getSecret: () => process.env.CSRF_SECRET,
cookieName: "__Host-csrf",
cookieOptions: { secure: true, sameSite: "strict", path: "/" },
});
app.use(doubleCsrfProtection);
app.get("/form", (req, res) => {
const token = generateToken(req, res);
res.render("form", { csrfToken: token });
});
The legacy csurf package is deprecated. Use csrf-csrf for the double-submit cookie pattern or csrf-sync for the synchronizer token pattern.
Django
Django includes CSRF protection by default via CsrfViewMiddleware. Ensure it is in your MIDDLEWARE list:
MIDDLEWARE = [
'django.middleware.csrf.CsrfViewMiddleware',
# ...
]
In templates, include the token in every form:
<form method="POST">
{% csrf_token %}
<!-- form fields -->
</form>
For AJAX requests, read the csrftoken cookie and include it as the X-CSRFToken header.
Laravel
Laravel includes CSRF protection via the VerifyCsrfToken middleware. In Blade templates:
<form method="POST" action="/transfer">
@csrf
<!-- form fields -->
</form>
The @csrf directive renders a hidden _token field. For JavaScript requests, Laravel also reads the token from the XSRF-TOKEN cookie and accepts it in the X-XSRF-TOKEN header.
Ruby on Rails
Rails enables CSRF protection by default. In ApplicationController:
class ApplicationController < ActionController::Base
protect_from_forgery with: :exception
end
Rails automatically injects a meta tag with the CSRF token. The Unobtrusive JavaScript driver (UJS/Turbo) reads it and includes it in AJAX requests. In form views, Rails inserts the authenticity_token field automatically with form helpers.
SameSite Cookies as Defense-in-Depth
The SameSite cookie attribute provides browser-level CSRF protection. When set to Lax, the browser does not send the cookie on cross-site POST requests, which blocks most CSRF attacks. With Strict, the cookie is not sent on any cross-site request.
Set-Cookie: session=abc123; SameSite=Lax; Secure; HttpOnly
SameSite is not a replacement for CSRF tokens. It is a defense-in-depth layer. Reasons to keep explicit CSRF tokens:
- Older browsers do not support
SameSite. - Subdomain attacks can bypass
SameSiteif the attacker controls a sibling subdomain. SameSite=Laxstill allows GET-based CSRF (top-level navigations send cookies).
Use both: SameSite=Lax on session cookies and CSRF tokens on all state-changing forms.
Verification
After implementing CSRF protection, test it by submitting a form without the token -- the server should reject it with a 403 or 422 response. Test with a cross-origin page that attempts to submit to your endpoints. Run a CyberShield scan to identify forms missing CSRF tokens, forms using GET for sensitive operations, and form actions pointing to insecure HTTP URLs. Form weaknesses are just one category that passive web vulnerability assessment can surface without touching a single exploit.
Continue Reading
Cookie Security: Secure, HttpOnly, and SameSite Flags
Protect session cookies from theft and CSRF attacks by configuring the Secure, HttpOnly, and SameSite flags correctly.
WAF Detection and Fingerprinting: How CyberShield Identifies Web Application Firewalls
Understanding what web application firewall protects a target is essential context for any security assessment. Learn how CyberShield passively fingerprints 15+ WAF vendors through header analysis, error patterns, and behavioral signatures.
Web Vulnerability Assessment: What Passive Analysis Reveals Without Firing a Single Exploit
Passive web analysis uncovers OWASP-relevant vulnerabilities -- information leaks, form weaknesses, exposed files, and redirect flaws -- without touching a single exploit.