Difficulty: 🟡 Practitioner
Goal:
- 🔍 Log in as
administrator:admin→ promote carlos via the admin panel → capturePOST /admin-rolesin Burp Repeater - 🍪 Log in as
wiener:peterin a private window → copy wiener's session cookie into the captured request → confirmPOSTis rejected with"Unauthorized" - 🔀 Change method to
POSTX→ response changes to"missing parameter"(proving the role check only fires onPOST) - ⚡ Right-click → "Change request method" → converts to
GETwith params in the query string → changeusername=carlostousername=wiener→ send → wiener is promoted to admin → lab solved!
🧠 Concept Recap
Method-based access control bypass occurs when a role check is tied to a specific HTTP method rather than to the action itself. The developer added an admin check that fires when the endpoint receives a
POST- but forgot to enforce the same check forGET(or any other method). SinceGETandPOSToften reach the same back-end handler and the same business logic, a non-admin can perform the privileged action simply by sending it as aGET. The method is irrelevant to what the code actually does — it only gates who can do it.
📊 What Each Method Triggers
══════════════════════════════════════════════════════════
🔬 METHOD SWITCHING — WHAT HAPPENS EACH WAY
══════════════════════════════════════════════════════════
Request Session Role Check Action
Method Fires? Executes?
──────────────────────────────────────────────────────
POST admin ✅ Yes — ✅ Yes
(normal passes
admin
promote)
POST wiener ✅ Yes — ❌ Blocked
fails → 401
POSTX wiener ❌ No check ⚠️ "missing
at all parameter"
GET wiener ❌ No check ✅ Action
at all executes
══════════════════════════════════════════════════════════
The flaw in one line:
→ Role check: if (method === 'POST' && !isAdmin) → reject
→ GET handler: processes the same upgrade logic with no role check at all
→ Changing POST → GET sidesteps the check entirely
Why POSTX matters as a probe:
→ POSTX is not a standard HTTP method
→ The role check rule says "if POST → check role"
→ POSTX is not POST → role check doesn't fire
→ Response changes from "Unauthorized" to "missing parameter"
→ This change proves the role check is method-specific, not action-specific
→ Now we know any non-POST method will bypass it - GET is the cleanest choiceThe full attack flow:
Phase 1 — Recon as admin:
Log in as administrator:admin
Admin panel → Upgrade user → promote carlos
Capture: POST /admin-roles?username=carlos&action=upgrade (with admin session)
Send to Burp Repeater ✅
Phase 2 - Confirm the block:
Log in as wiener:peter (private window) → copy wiener's session cookie
Paste wiener's cookie into the captured request → Send
Response: "Unauthorized" ✅ (POST is protected)
Phase 3 - Probe the method check:
Change method: POST → POSTX → Send
Response: "missing parameter" (not "Unauthorized")
✅ Proves: role check only fires on POST
Phase 4 - Bypass:
Right-click → "Change request method" → converts to GET
Burp moves params to query string: GET /admin-roles?username=carlos&action=upgrade
Change username=carlos → username=wiener → Send
Response: 200 / 302 - wiener is now admin ✅
Lab solved! 🎉🛠️ Step-by-Step Attack
🔧 Step 1 — Log In as Admin and Capture the Promote Request
- 🌐 Click "Access the lab"
- 🔌 Ensure Burp Suite is running and proxying traffic
- 🖱️ Log in as
administrator/admin - 🖱️ Navigate to the admin panel (it should be visible in the nav as you're logged in as admin)
- 🖱️ Click "Upgrade user" next to
carlos— this triggers the promote action
The Upgrade button sends a POST request to the admin endpoint.
We're doing this as admin to capture a WORKING request structure —
we need the exact URL, parameters, and format before we attempt the bypass.6. 🖱️ In Burp → Proxy → HTTP history → find POST /admin-roles with username=carlos
7. 🖱️ Right-click → "Send to Repeater"
The captured request looks like:
POST /admin-roles?username=carlos&action=upgrade HTTP/1.1
Host: YOUR-LAB-ID.web-security-academy.net
Cookie: session=ADMIN-SESSION-COOKIE
Content-Length: 0
(or the username may be in the body - either way, note the structure)
Keep this tab open in Repeater.
Do NOT close it - we'll modify it in subsequent steps.🔧 Step 2 — Get Wiener's Session Cookie
- 🖱️ Open a private / incognito browser window — this keeps sessions separate
- 🖱️ Log in as
wiener/peterin the incognito window - 🖱️ In Burp → Proxy → HTTP history → find any recent request from the incognito session → look at the
Cookieheader:
Cookie: session=WIENER-SESSION-COOKIE-VALUE4. 🖱️ Copy wiener's session cookie value
Why a private window?
→ Your main browser is still logged in as admin
→ A private window creates a separate session
→ This gives us wiener's cookie without logging out of admin
→ We need both sessions simultaneously:
- Admin session: to have a working captured request
- Wiener's session: to replay that request as a non-admin🔧 Step 3 — Confirm POST is Blocked for Non-Admins
- 🖱️ In Burp Repeater, in the
POST /admin-rolesrequest:
- Find the
Cookie:header - Replace the admin session value with wiener's session cookie:
POST /admin-roles?username=carlos&action=upgrade HTTP/1.1
Host: YOUR-LAB-ID.web-security-academy.net
Cookie: session=WIENER-SESSION-COOKIE2. 🖱️ Click "Send"
Expected response:
→ "Unauthorized" (401 or similar)
→ The POST endpoint checks the role → wiener is not admin → rejected ✅
This confirms:
→ The POST endpoint HAS an access control check
→ The check correctly rejects non-admins for POST requests
→ But the question is: does the check apply to ALL methods, or only POST?🔧 Step 4 — Probe: Change Method to POSTX
- 🖱️ In Burp Repeater, manually edit the first line of the request:
- Change
POSTtoPOSTX:
POSTX /admin-roles?username=carlos&action=upgrade HTTP/1.1
Host: YOUR-LAB-ID.web-security-academy.net
Cookie: session=WIENER-SESSION-COOKIE2. 🖱️ Click "Send"
Expected response:
→ "missing parameter" (or similar error — NOT "Unauthorized")
Why this is significant:
→ The response changed from "Unauthorized" to "missing parameter"
→ "Unauthorized" = role check fired and rejected wiener
→ "missing parameter" = role check did NOT fire - a different handler ran
→ The role check rule is: if (method === 'POST') → check role
→ POSTX ≠ POST → the rule doesn't match → check is skipped
→ The server reached actual business logic but failed on input validation
→ This proves conclusively: the role check is method-specific
→ Any non-POST method bypasses it
→ GET is the standard alternative - parameters move to the query string🔧 Step 5 — Convert to GET and Promote Wiener
- 🖱️ Right-click anywhere in the Repeater request panel → "Change request method"
What Burp does automatically:
→ Changes POST → GET
→ Moves any body parameters to the query string
→ Result:
GET /admin-roles?username=carlos&action=upgrade HTTP/1.1
Host: YOUR-LAB-ID.web-security-academy.net
Cookie: session=WIENER-SESSION-COOKIE2. 🖱️ Change username=carlos to username=wiener in the query string:
GET /admin-roles?username=wiener&action=upgrade HTTP/1.1
Host: YOUR-LAB-ID.web-security-academy.net
Cookie: session=WIENER-SESSION-COOKIE3. 🖱️ Click "Send"
Expected response:
→ 200 OK or 302 redirect — no "Unauthorized"
→ The upgrade action executes for wiener
→ The role check never fired — it only guards POST, not GET
What just happened:
→ The GET handler for /admin-roles has no role check
→ It ran the same upgrade business logic as POST would
→ wiener's account was promoted to admin in the database🔧 Step 6 — Verify Admin Access
- 🖱️ In the incognito window (logged in as wiener), navigate to the admin panel:
https://YOUR-LAB-ID.web-security-academy.net/admin
Expected result:
→ Admin panel loads for wiener ✅
→ wiener is now an administrator
→ Lab solved! 🎉🎉 Lab Solved!
✅ Captured POST /admin-roles as admin
✅ Confirmed POST is blocked for wiener (Unauthorized)
✅ Changed to POSTX → response changed to "missing parameter" (role check bypassed)
✅ Changed to GET + username=wiener → upgrade executed — wiener is now admin
✅ Lab complete!🔗 Complete Attack Chain
1. Log in as admin → promote carlos → capture POST /admin-roles → Repeater
2. Swap to wiener's session cookie → POST still blocked → "Unauthorized" ✅
3. Change POST → POSTX → "missing parameter" (proves check is method-specific)
4. Right-click → "Change request method" → GET
5. Change username=carlos → username=wiener → Send → 200 OK → wiener promoted
6. Verify: /admin loads for wiener → lab solved🧠 Deep Understanding
Why does the role check only fire on POST?
The developer probably wrote something like this:
// Middleware / route guard
app.post('/admin-roles', requireAdmin, handleUpgrade);
app.get('/admin-roles', handleUpgrade); // ← no requireAdmin here!
Or using a framework with method-based routing:
@PostMapping("/admin-roles")
@PreAuthorize("hasRole('ADMIN')") // ← role check tied to POST mapping
public Response upgradeUser(String username) { ... }
@GetMapping("/admin-roles")
public Response upgradeUser(String username) { ... } // ← no @PreAuthorize
The developer secured the POST route and forgot the GET route.
Both routes call the same handleUpgrade / upgradeUser business logic.
The business logic doesn't care about the caller's role - it just executes.The POSTX probe — why it's the key diagnostic step
The POSTX step is not just curiosity — it's a precise diagnostic:
POST + wiener → "Unauthorized" → role check FIRES for POST
POSTX + wiener → "missing param" → role check DOES NOT FIRE for POSTX
GET + wiener → 200 (executes) → role check DOES NOT FIRE for GET
The change from "Unauthorized" to "missing parameter" is the signal:
→ "Unauthorized" = access control rejected the request
→ "missing parameter" = access control was SKIPPED, actual handler ran
→ The handler ran but complained about input - wiener reached the business logic
Without the POSTX probe, you could go straight to GET and it would still work.
But the probe gives you evidence of WHY it works - valuable for understanding
and for reporting: "The role check is only bound to the POST method handler."Method override as an alternative bypass
Some frameworks also honour method override headers:
X-HTTP-Method-Override: GET
X-Method-Override: GET
_method=GET (in POST body)
These let a POST request be treated as a GET by the framework:
POST /admin-roles HTTP/1.1
X-HTTP-Method-Override: GET
Cookie: session=wiener-session
→ Framework sees: treat this as GET
→ GET handler runs → no role check → upgrade executes
This is relevant when:
→ The server only supports POST (e.g., from a browser form)
→ But the back-end framework supports method override headers
→ Some WAFs block GET to sensitive endpoints but allow POST with override header
In this lab, plain GET works - no override header needed.Lab 7 vs Lab 8 — Two platform-level bypass techniques
Lab 7 (X-Original-URL):
→ Front-end sees a different URL than the back-end
→ Bypass: deceive the front-end about which URL is being accessed
→ Attack layer: routing / URL dispatch
Lab 8 (method change):
→ Role check is bound to one HTTP method, not the action
→ Bypass: use a different method to reach the same handler without the check
→ Attack layer: method-based middleware / route guards
Both share the same root cause:
→ Access control is implemented at the wrong level of abstraction
→ It should be on the ACTION (the business logic function itself)
→ Not on the URL pattern (Lab 7) or the HTTP method (Lab 8)🐛 Troubleshooting
Problem: POSTX returns "Unauthorized" (same as POST) — not "missing parameter"
→ This framework treats unknown methods the same as POST for routing
→ Skip straight to GET — the POSTX probe gave you the same result as POST
→ Change method to GET and try — the role check may still only guard POST
Problem: GET returns "Unauthorized" too - not 200
→ The GET handler also has a role check - method-based bypass won't work here
→ Try: X-HTTP-Method-Override: GET on a POST request
→ Try: HEAD, PUT, PATCH methods
→ The vulnerability may require a different bypass technique
Problem: Can't find wiener's session cookie in Proxy history
→ Ensure Burp is intercepting the incognito window (check proxy settings)
→ Navigate to /my-account in the incognito window to generate a request
→ The session cookie is in the Cookie: header of any request from that window
Problem: After sending GET with username=wiener, admin panel doesn't load for wiener
→ The request may have used the wrong session - confirm Cookie: is wiener's session
→ Hard-refresh the incognito window (Ctrl+Shift+R) to clear any cached auth state
→ Check: the GET response should be 200 or 302 (not 401/403)
Problem: Burp's "Change request method" doesn't appear in right-click menu
→ Right-click inside the request editor panel (not the tab or header area)
→ Alternatively: manually type GET and move params from body to ?param=value in URL💬 In One Line
🔀 The admin role check only guarded POST requests — so changing the method to GET reached the same upgrade handler with no check at all, promoting wiener to admin with a non-admin session. That's Method-Based Access Control Bypass — securing the method, not the action.
🔒 How to Fix It
// Priority 1 — Bind the role check to the ACTION, not the method
// Never assume the method is a reliable security boundary
// ❌ BAD - role check only on POST route:
app.post('/admin-roles', requireAdmin, handleUpgrade);
app.get('/admin-roles', handleUpgrade); // ← no check!
// ✅ GOOD - role check on every route that reaches the action:
app.post('/admin-roles', requireAdmin, handleUpgrade);
app.get('/admin-roles', requireAdmin, handleUpgrade);
// Or better - put the role check INSIDE the handler itself:
function handleUpgrade(req, res) {
if (!req.session || req.session.role !== 'admin') {
return res.status(403).send('Forbidden');
}
// proceed with upgrade...
}
// Now it doesn't matter what method is used - the handler always checks
// Priority 2 — Restrict which HTTP methods the endpoint accepts at all
// If the endpoint only needs POST, explicitly reject everything else
app.all('/admin-roles', (req, res, next) => {
if (req.method !== 'POST') {
return res.status(405).send('Method Not Allowed');
}
next();
});
app.post('/admin-roles', requireAdmin, handleUpgrade);
// Or in Express:
// app.use('/admin-roles', methodNotAllowed(['POST']));
// Any GET/POSTX/PUT/etc. → 405 immediately, never reaches the handler
// Priority 3 — Reject method override headers from untrusted clients
// Prevent X-HTTP-Method-Override from being used to tunnel methods
app.use((req, res, next) => {
// If client sends a method override header, ignore it
delete req.headers['x-http-method-override'];
delete req.headers['x-method-override'];
// Don't use req.body._method to override the HTTP method
next();
});
// Framework-level: disable method override middleware if your framework supports it
// Express: don't use method-override npm package unless intentionally needed
// Priority 4 — In frameworks with annotation-based routing, secure all method variants
// Java Spring - ❌ BAD: only POST is secured:
@PostMapping("/admin-roles")
@PreAuthorize("hasRole('ADMIN')")
public ResponseEntity upgrade(@RequestParam String username) { ... }
@GetMapping("/admin-roles") // ← no @PreAuthorize
public ResponseEntity upgrade(@RequestParam String username) { ... }
// ✅ GOOD: Apply security at the method level and lock down all HTTP methods:
@RequestMapping(value = "/admin-roles", method = {RequestMethod.POST})
@PreAuthorize("hasRole('ADMIN')")
public ResponseEntity upgrade(@RequestParam String username) { ... }
// Or: use a class-level @PreAuthorize on the AdminController class itself👏 If this helped you — clap it up (you can clap up to 50 times!)
🔔 Follow for more writeups — dropping soon
🔗 Share with your pentest team
💬 Drop a comment