An API endpoint that deletes a resource is protected. The authorization check fires on DELETE. It does not fire on HEAD. Both reach the same handler.
This gap exists because most access controls assume the HTTP verb is immutable from the client's perspective. That assumption is false. The client chooses the verb. The server accepts whatever arrives and routes based on it.
Per-method auth is a whitelist with guaranteed gaps
Authorization tied to the HTTP verb creates an implicit allow for every method absent from the check. REST frameworks route all verbs to the same handler unless the developer adds explicit restrictions to each route.
The vulnerable pattern in Express:
// Vulnerable: auth only on DELETE, HEAD matches GET route
app.delete('/api/users/:id', requireAdmin, deleteUser) // guarded
app.get('/api/users/:id', getUser) // no auth — HEAD auto-matches this
// HEAD /api/users/123 bypasses requireAdmin on DELETE by routing through GET
// app.get() auto-registers HEAD; app.delete() does NOT — only app.get()
HEAD bypasses the DELETE guard by routing through the unprotected GET route, which Express auto-maps to HEAD. Catching all verbs requires app.use() or app.all(). The gap between routing scope and authentication scope is where the bypass happens, silently, without errors or alerts.
CWE-302 names this pattern: "Authentication Bypass by Assumed-Immutable Data." The system assumes the HTTP method is immutable from the client's perspective. It is not. CAPEC-274 documents that arbitrary strings as HTTP verbs bypass method-based filters on certain servers: the filter checks GET and POST; FUZZ passes through.
The root condition is structural. Method-based authorization lists what is denied for specific verbs. Resource-based authorization lists what is allowed, regardless of the verb. A blocklist requires predicting every verb the attacker might use; an allowlist rejects anything not explicitly permitted.
Four bypass vectors, four broken assumptions
Each vector exploits a different developer assumption. HEAD is considered harmless. OPTIONS is informational. TRACE is forgotten legacy. X-HTTP-Method-Override exists for compatibility with old clients. None of them receive the same security scrutiny as POST and DELETE during development.
HEAD: The server returns identical headers to a GET response, with no body. This confirms resource existence, leaks ETag, Last-Modified, and Content-Type, and bypasses WAF rules that inspect only the response body. ETag values returned from HEAD requests without a valid session confirm the resource is being served without authentication enforcement. In poorly written handlers, HEAD triggers state changes when the code does not check the method before executing the operation.
OPTIONS: The CORS preflight exposes Access-Control-Allow-Methods with every method accepted by the endpoint. An attacker reads this response and gets the complete list of valid verbs to test, without guessing.
TRACE: Echoes request headers in the response. The Cross-Site Tracing (XST) technique uses this to read HttpOnly cookies via JavaScript. Modern browsers block TRACE by default. API testing tools, curl, and any HTTP client without browser security policies still send TRACE without restriction. Internal services communicating over plain HTTP are frequent targets.
X-HTTP-Method-Override: Originally created for SOAP clients and old browsers unable to send DELETE or PUT. Rails, Django REST Framework, Spring, and many API gateways process this header without re-running authentication middleware for the overridden method. A POST with X-HTTP-Method-Override: DELETE reaches the DELETE handler without passing through the authentication check configured for the original POST method.
It shipped in production: CVE-2023-30845 and Apache Tomcat
CVE-2023-30845 hit Google's ESPv2 with CVSS 8.2, filed as GHSA-6qmp-9p95-fc5f. ESPv2 validated the JWT for the original request method and then routed to the overridden method's handler via X-HTTP-Method-Override. The authentication system did not re-validate the JWT for the target method. Affected versions: v2.20.0 through v2.42.0. Fixed in v2.43.0.
The precise mechanism: the proxy inspected the original HTTP method to validate the JWT scope. After validation, it processed the X-HTTP-Method-Override header and redirected to the corresponding handler without re-running authentication. An attacker sent a POST with a valid JWT for the POST scope plus X-HTTP-Method-Override: DELETE, reaching protected DELETE operations.
Apache Tomcat illustrates the same pattern at the application server level. Security constraints defined in web.xml apply to exact HTTP method strings. A constraint on DELETE does not cover delete in lowercase or X-HTTP-Method-Override: DELETE, and method normalization must be explicit. Without it, an override header processes as POST while the constraint checks only DELETE, and access reaches the protected resource.
CVE-2023-30845 and the documented Tomcat behavior share the same root cause. The component that checks authentication and the component that processes the HTTP verb are not the same code. That separation creates a window where the effective verb diverges from the authenticated verb. The fix requires authentication to re-run after any method translation, not just at the point where the original request arrives.
Detection: nine-method fuzzing on every authenticated endpoint
Systematic coverage of all RFC 9110 methods plus X-HTTP-Method-Override variants against every authenticated endpoint exposes authorization gaps in minutes. Every API security assessment must include this coverage.
The nine methods: GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS, TRACE, CONNECT. Add arbitrary strings (FUZZ, MPRV) to detect wildcard handlers. For each endpoint, send requests with a valid session and without a session for each method, then compare HTTP status codes and response sizes.
The override header variants: X-HTTP-Method-Override, X-HTTP-Method, X-Method-Override, and the _method query parameter. Each is accepted by different frameworks. The absence of acceptance for one header does not guarantee the others are also rejected.
Signals to watch: different HTTP status between methods (200 vs 403 or 405). A 200 response without a valid session for a non-standard method confirms a direct bypass. HEAD responses must return identical header size to GET, confirming access to the same resource. Methods listed in the OPTIONS Access-Control-Allow-Methods response with no authentication check of their own are candidates for bypass. OWASP WSTG OTG-CONFIG-006 specifies comparing HEAD against GET as a required test for each endpoint. Nuclei templates from projectdiscovery/nuclei-templates cover automatic JWT bypass detection via X-HTTP-Method-Override.
The fix is resource-level auth, not a longer verb blocklist
Disabling TRACE and blocking override headers at the gateway is necessary hygiene. The architectural fix is moving authorization to resource scope, ensuring the check fires regardless of which HTTP verb the client presents.
The direct change:
// Before: auth tied to the method
app.delete('/api/users/:id', requireAdmin, deleteUser);
// After: auth at resource scope, behavior by method
app.all('/api/users/:id', requireAdmin, (req, res) => {
if (req.method === 'DELETE') return deleteUser(req, res);
if (req.method === 'GET') return getUser(req, res);
res.status(405).end();
});
At the gateway, disable X-HTTP-Method-Override unless legitimate clients require it. In nginx: proxy_set_header X-HTTP-Method-Override "". Configure an explicit method allowlist per route and return HTTP 405 for every method outside that list, before the request reaches the application server. Gateway enforcement works because it applies before any application code runs. A filter covering every route by default is harder to miss than middleware added per route.
The OWASP REST Security Cheat Sheet specifies: an allowlist of permitted HTTP methods per endpoint, with HTTP 405 rejection for everything outside the list. This eliminates the implicit allow-all gap that every per-method whitelist carries. The MAGO Intel tool (intel.mago.team) runs the nine-method fuzzing matrix against every discovered endpoint. The system detects X-HTTP-Method-Override acceptance patterns during API surface analysis.
The check that protects DELETE must also protect HEAD reaching the same resource. If that sentence requires a code change, the authorization model is not complete.
Top comments (0)