Auto-submitting HTML form crafted by the attacker that sends a POST request with victim credentials when the page loads.
TL;DR
multipart/form-data and application/x-www-form-urlencoded are CORS simple requests — no preflightPOST-based CSRF (CWE-352) is the canonical form of cross-site request forgery. An attacker hosts a page containing a hidden HTML form targeting a state-changing endpoint on the victim's authenticated application. The form auto-submits via JavaScript on page load, causing the victim's browser to issue a POST request with all valid session cookies attached. The target server receives an authenticated, correctly-structured POST and — finding no CSRF token validation — executes the action.
This variant remains relevant despite SameSite=Lax becoming the Chrome default for cookies without an explicit attribute. Many applications still use explicit SameSite=None (required for cross-site embeds like payment widgets), and many legacy applications never set any SameSite attribute. More critically, even SameSite=Lax-protected applications are vulnerable when they fail to implement CSRF tokens and rely on SameSite alone — since SameSite is a browser-level defense that does not protect against sophisticated bypass chains.
The token omission bypass is the most prevalent implementation flaw. Many developers implement CSRF token validation as: "if the token field is present, validate it." This creates a bypass: an attacker who removes the token field entirely from the request body bypasses validation completely. The fix is strictly: "reject any state-changing request that does not include a valid token."
The auto-submitting form technique exploits the browser's willingness to send form-encoded POST requests cross-origin. Unlike fetch() and XMLHttpRequest (which trigger CORS preflight for non-simple methods), HTML form submissions with application/x-www-form-urlencoded and multipart/form-data are "simple requests" under the CORS specification and receive no preflight treatment.
Browser CORS simple request matrix:
| Content-Type | Triggers Preflight? | CSRF Possible? |
|---|---|---|
application/x-www-form-urlencoded | No | Yes |
multipart/form-data | No | Yes |
text/plain | No | Yes (JSON body trick) |
application/json | Yes | No (unless CORS misconfigured) |
Any custom header (e.g., X-CSRF-Token) | Yes | No |
Basic POST CSRF — auto-submitting form:
<!-- Attacker's page — victim visits and instantly submits this form -->
<html>
<body onload="document.getElementById('csrf').submit()">
<form id="csrf" action="https://target.com/account/password" method="POST"
style="display:none;">
<input type="hidden" name="new_password" value="Attackerpass1!">
<input type="hidden" name="confirm_password" value="Attackerpass1!">
<!-- Note: no csrf_token field included — testing omission bypass -->
</form>
</body>
</html>Multipart/form-data CSRF — for file upload endpoints:
<!-- File upload endpoints accept multipart — also a simple request -->
<form action="https://target.com/api/profile/avatar" method="POST"
enctype="multipart/form-data">
<input type="hidden" name="action" value="update">
<!-- Omit file field — server may still process action parameter -->
</form>
<script>document.forms[0].submit();</script>Token omission bypass — removing the CSRF field:
# VULNERABLE validation (token omission accepted):
def validate_csrf(request):
token = request.POST.get("csrf_token")
if token is None:
return # BUG: no token field = skip validation entirely
if token != session["csrf_token"]:
raise Forbidden("CSRF token invalid")
# SAFE validation (token presence mandatory):
def validate_csrf(request):
token = request.POST.get("csrf_token")
if not token or not secrets.compare_digest(token, session["csrf_token"]):
raise Forbidden("CSRF token missing or invalid")| Variant | Technique | Exploits |
|---|---|---|
| Auto-submit form | onload=form.submit() | Missing CSRF token or token omission bypass |
| Multipart CSRF | enctype="multipart/form-data" | CORS simple request — no preflight for file upload CSRF |
| Token omission | Remove token field from body | Server validates only when field is present |
| Token forgery | Submit random/blank token | Server uses weak comparison or non-session-bound token |
| Cross-user token | Reuse another user's valid token | Token not bound to session (global token pool) |
| Double-submit naive | Inject cookie via subdomain, match parameter | No HMAC binding on double-submit pattern |
The cross-user token variant is particularly damaging in enterprise applications. If the server validates that the submitted CSRF token exists in a global pool of valid tokens (rather than matching the specific session's token), an authenticated attacker can harvest their own valid CSRF token and use it to forge requests as any other user.
The naive double-submit cookie pattern (random value in cookie, same value in request parameter) is vulnerable when an attacker controls a subdomain. The attacker injects a cookie via document.cookie = 'csrf=attacker_value; domain=.target.com' from sub.target.com, then submits a form with csrf_param=attacker_value. Without HMAC binding, the server's double-submit check passes.
CVE-2023-47640 — Grails Framework (CVSS 8.8, High): Grails < 5.3.4 built-in CSRF protection was bypassed by omitting the CSRF token field from POST requests. The framework validated the token only when the field was included in the request, making the entire CSRF protection layer optional for any attacker who knew to remove the field. Every CSRF-protected Grails endpoint was exploitable until the framework was upgraded. CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:N = 8.8.
CVE-2023-29919 — OpenEMR Patient Portal (CVSS 8.8, High): CSRF on the email change endpoint in OpenEMR's patient portal used the same token omission pattern — the token field was only validated when present. An attacker could change a patient's email address via a crafted page, then trigger a password reset to the attacker's email, achieving full account takeover with access to protected health information (PHI). The CSRF vector combined with the password reset flow elevated severity to full ATO.
CVE-2022-24734 — MyBB Forum (CVSS 8.8, High): CSRF in the MyBB admin panel allowed attacker-controlled file write via crafted form submission. Root cause: SameSite=None on admin session cookies (allowing cross-origin POST) combined with no CSRF token validation on the file upload action. The file write vector led to remote code execution. Fixed in MyBB 1.8.30.
CVE-2024-21690 — Atlassian Confluence CSRF + XSS (CVSS 8.2, High): A combined CSRF and reflected XSS vulnerability in Confluence Data Center and Server 7.19.0–8.9.0 allowed unauthenticated attackers to both inject arbitrary JavaScript and compel authenticated users to perform unintended state-changing actions. Affected versions up to 8.9.0; fixed in 7.19.26+, 8.5.14+, 9.0.1+.
application/x-www-form-urlencoded to multipart/form-data. Many servers accept both; multipart may bypass additional validation.Burp Suite Pro's active scanner flags forms without CSRF tokens. OWASP ZAP rule 20012 (Anti-CSRF Tokens Scanner) identifies state-changing requests lacking token protection. Neither tool automatically tests the token omission bypass — Burp's active scanner submits the token it finds in the form, not omits it. XSRFProbe is purpose-built for omission, blank, and cross-user token testing.
BreachVex tests token omission: after identifying forms with CSRF token fields via HTML parsing, it resubmits the same POST request with the token field removed. A 2xx response without rejection keywords (e.g., "invalid token", "forbidden", "CSRF") in the body constitutes a confirmed bypass finding.
# Rails ApplicationController — protect_from_forgery is ON by default
class ApplicationController < ActionController::Base
# DO NOT add: protect_from_forgery except: [:update_email, ...]
# DO NOT add: skip_before_action :verify_authenticity_token
end
# Safe form — Rails auto-injects authenticity_token
# <%= form_with(url: change_password_path) do |f| %>
# <%= f.password_field :new_password %>
# <%= f.submit "Change Password" %>
# <% end %>
# Rails API mode (ActionController::API) does NOT include CSRF protection
# Explicitly add it for session-cookie-authenticated API controllers:
class Api::AccountsController < ActionController::API
include ActionController::RequestForgeryProtection
protect_from_forgery with: :exception
end# settings.py — CsrfViewMiddleware is in MIDDLEWARE by default
# Do NOT add to MIDDLEWARE_CLASSES excluded list
# Do NOT use @csrf_exempt on state-changing views
# VULNERABLE — explicit exemption
from django.views.decorators.csrf import csrf_exempt
@csrf_exempt
def change_email(request): # DO NOT DO THIS
...
# SAFE — rely on middleware (enabled by default)
def change_email(request):
if request.method == 'POST':
user.email = request.POST['email']
user.save()
return JsonResponse({'ok': True})// Django AJAX — include CSRF token in headers
function getCsrfToken() {
return document.cookie.split('; ')
.find(row => row.startsWith('csrftoken='))
?.split('=')[1];
}
fetch('/account/email', {
method: 'POST',
headers: {
'X-CSRFToken': getCsrfToken(),
'Content-Type': 'application/json',
},
body: JSON.stringify({ email: newEmail }),
credentials: 'same-origin',
});// app/Http/Middleware/VerifyCsrfToken.php
class VerifyCsrfToken extends Middleware {
// VULNERABLE: exempting payment and profile endpoints
// protected $except = ['api/payment/*', 'account/profile']; // DO NOT DO THIS
// SAFE: empty $except array — all routes protected
protected $except = [];
}{{-- resources/views/account/email.blade.php --}}
<form action="/account/email" method="POST">
@csrf
{{-- Expands to: <input type="hidden" name="_token" value="..."> --}}
<input type="email" name="email" placeholder="New email">
<button type="submit">Update Email</button>
</form>@Configuration
public class SecurityConfig {
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
// VULNERABLE: many REST API guides recommend disabling CSRF entirely
// http.csrf(csrf -> csrf.disable()); // DO NOT DO THIS for cookie-auth apps
// SAFE: CookieCsrfTokenRepository for SPAs (readable by JavaScript)
http.csrf(csrf -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
.csrfTokenRequestHandler(new XorCsrfTokenRequestAttributeHandler())
);
return http.build();
}
}The most dangerous implementation mistake is validating CSRF tokens only when they are present. "Missing token = skip check" means any attacker who knows to omit the field bypasses the entire protection. The correct logic: missing token = 403 Forbidden, invalid token = 403 Forbidden, valid token = proceed.
The attacker creates an HTML page containing a hidden form pointing to the target's state-changing endpoint. JavaScript triggers the form submission automatically when the victim loads the page. The browser attaches the victim's session cookies to the POST request, and the server processes it as if it were legitimately initiated by the user.
Yes, SameSite=Lax blocks cross-site POST form submissions. This is the primary reason Chrome's adoption of Lax as the default for cookies without explicit SameSite attributes significantly reduced POST-based CSRF prevalence. However, applications still need CSRF tokens if they cannot guarantee all cookies have explicit SameSite=Lax or Strict.
Many frameworks validate a CSRF token only when the token field is present in the request. If the field is omitted entirely—not submitted—the server skips validation and accepts the request. CVE-2023-47640 (Grails, CVSS 8.8) and CVE-2023-29919 (OpenEMR, CVSS 8.8) both demonstrate this pattern: token presence is optional, not mandatory.
Yes. multipart/form-data is a 'simple' Content-Type under CORS rules—it does not trigger a preflight OPTIONS request. An attacker can craft a form with enctype='multipart/form-data' and POST it cross-site. If the server accepts multipart-encoded data on an endpoint that normally uses JSON or form-urlencoded, CSRF is possible.
Token omission: the attacker simply does not include the token field at all. If the server only validates when the field is present, the request succeeds with no token. Token forgery: the attacker submits an incorrect token value. Most servers that implement tokens properly reject forgery but may incorrectly accept omission. Both should return HTTP 403.
Intercept the legitimate state-changing POST request in Burp Suite. Send to Repeater. Remove the csrf_token (or authenticity_token, _token, __RequestVerificationToken) field entirely from the request body. Click Send. If the server returns 200 or a success redirect instead of 403, the token omission bypass is confirmed.
Django enables CsrfViewMiddleware globally by default—every non-safe request requires a valid csrfmiddlewaretoken. Rails enables protect_from_forgery in ApplicationController by default. Laravel's web middleware group includes VerifyCsrfToken. Spring Security requires explicit configuration (not on by default for REST APIs).
Laravel's $except array in VerifyCsrfToken middleware exempts specific routes. Many developers exempt entire API route groups (api/*) from CSRF checks, assuming JWT or API key authentication. If those endpoints also accept session-cookie authentication (which Laravel supports), cross-site POST requests with a session cookie succeed without a CSRF token.