BFLA Survives Pentests: Why Testers Never Try the Wrong HTTP Method
Jon Bottarini opened Burp Suite, captured a GET from a restricted user session, and changed the method to POST. The request hit /api/v1/alerts/conditions on New Relic Synthetics and the server returned 200. With no Synthetics privileges, a restricted user had just created admin-level alert conditions. The technique was six years old when CVE-2025-27507 showed that 12 endpoints in ZITADEL's Admin API followed the same pattern, this time at CVSS 9.0. The direct impact: an attacker could redirect all LDAP logins to a malicious server.
The pattern survives because standard pentests do not test for it.
Why Standard Pentests Miss BFLA
The tester follows the API documentation. GET /api/users gets a BOLA test: swap the user ID for another user's. POST /api/users gets a privilege escalation test via payload manipulation. Both tests stay on the documented method, because that is what the engagement scope describes.
The server's authorization logic was written for the documented method. POST /api/users has a role=admin check. GET /api/users was added later for reporting, and the check was copied partially or never written at all. The code for one endpoint implies nothing about the other.
CVE-2025-27507 is the clearest example. The 12 ZITADEL Admin API endpoints were not undocumented. The documentation described them as admin-only, but specific HTTP methods made them reachable without IAM manager roles. The tester who validated the path with admin credentials never tried alternative methods with a standard user token. The gap stayed open.
Mislabeling compounds the problem. The GitHub Advisory for CVE-2025-27507 carries the title "IDOR vulnerabilities in ZITADEL Admin API." The root cause is BFLA: unauthorized function invocation, not object enumeration via ID manipulation. IDOR manipulates an object ID to reach another user's data at the same privilege level. BFLA calls a function the attacker's role cannot execute at all. When scanners tag both as generic access control failures, BFLA-specific test cases like HTTP method fuzzing and role-function matrices get replaced by IDOR enumeration tests. The result is a documented false negative.
H1:334143: A GET Becomes POST and Opens an Admin Function
Jon Bottarini was a restricted user on New Relic Synthetics in 2018. Synthetics accounts had no access to the main platform's alert policies, an intentional constraint in the permission model. He captured GET /api/v1/alerts/conditions in Burp Suite and changed the method to POST.
POST /api/v1/alerts/conditions HTTP/1.1
Host: api.newrelic.com
X-Api-Key: <restricted-user-key>
Content-Type: application/json
{"name": "Test Condition", "type": "apm_app_metric", "entities": [...]}
The server checked authorization on the path /api/v1/alerts/conditions. It did not check the method-path pair. GET was a read, permitted. POST was resource creation, an admin function. The authorization check for POST did not exist. The server returned 200, created the alert condition, and the bounty paid out at $750.
The pattern repeats across modern APIs for a structural reason. REST frameworks route GET and POST to different handler functions, each with its own authorization decorator. In Express.js this is router.get() and router.post(). In FastAPI, these are functions tagged with @app.get and @app.post. In Django REST Framework, they are get() and post() methods in the same view class with separate dispatch logic. Developers add authorization to the primary handler and skip the alternate method because the two look like distinct entities in the code.
The attack surface is not the path. It is the method-path pair. Each combination is an independent authorization gate, and most handlers only protect the combination the documentation describes.
CVE-2024-55073: Mealie and the PUT With No Guard
Mealie is an open source recipe manager with multi-user groups and role-based permissions. The permission model distinguishes group members, group managers, and household admins. The endpoint PUT /api/v1/users/{id}/profile exists to update display name and avatar.
The request body accepted the boolean fields canManage, canInvite, and canOrganize, fields that only a household admin should normally modify. The PUT handler validated that the requesting user belonged to the group, but did not validate whether canManage permission was required to change those flags. The GET /api/v1/users handler had the correct check in place.
PUT /api/v1/users/{own-user-id}/profile HTTP/1.1
Authorization: Bearer <group-manager-token>
Content-Type: application/json
{"canManage": true, "canInvite": true, "canOrganize": true}
A group manager sent PUT with canManage=true on their own user ID and became group admin without approval from a superior admin. CVSS 7.6 because impact stays within group scope. The same Mealie release carried CVE-2024-55072 (CVSS 5.4) on PUT /api/households/permissions, the same pattern on a different endpoint in the same release.
Two CVEs in the same application, same release, both via PUT. The team patched the GET handler and left the PUT unguarded. The audit that found one did not look for the other on the alternate method.
Four Methods Testers Never Try
The standard tester checks GET and POST on documented endpoints. Four combinations get missed systematically, and each has a documented exploitation history.
| Method Pair | Authorization Risk | Example |
|---|---|---|
| GET to POST | Admin write function reachable by restricted user | H1:334143 (New Relic) |
| GET to DELETE | Destructive function with no separate gate | Active route without DELETE in documentation |
| PUT to PATCH | PATCH may bypass field validation that PUT enforces | CVE-2024-55073 (Mealie) |
| OPTIONS to any | Preflight returns full method list; each is a test candidate | ZITADEL Admin API |
The Allow header from an OPTIONS response is the starting point. When the server replies Allow: GET, POST, DELETE, that is the full surface to test, not just what the documentation describes. The server announces the methods it accepts, independent of what the published documentation says.
The PUT-to-PATCH pair deserves specific attention. PATCH is frequently implemented as a shortcut for updating individual fields, with less rigorous validation than a full PUT. In implementations that share the same handler with method-based branching logic, PATCH can bypass field-level checks that PUT enforces.
Deprecated API versions add another surface. An endpoint at /v1/resource removed from documentation may still be routed on the server. Authorization middleware added to /v2/resource does not retroactively protect /v1/resource. CVE-2025-27507 and both Mealie CVEs confirm that authorization patches are applied to the current version and rarely reviewed against older versions.
The Probe: OPTIONS and Systematic Method Fuzzing
Three passes detect BFLA where standard pentests stop. Each pass uncovers a surface the documentation does not describe.
Pass 1: send OPTIONS to each discovered endpoint and parse the Allow header. The server discloses the full list of methods it accepts on that path. This is not endpoint enumeration; it is the surface the server announces before any authentication challenge.
curl -X OPTIONS https://api.target.com/api/v1/users -i | grep -i allow
# Allow: GET, POST, DELETE
Pass 2: for each non-GET method in the Allow header, send the request with your current low-privilege session token. 200 or 201 = confirmed BFLA, the function executed without a gate. 403 = gate present and functional. 405 = method blocked at the framework or web server level, not exploitable this way. The distinction between 403 and 405 determines whether the block is authorization or routing.
Pass 3: replace /v2/ with /v1/, and /v3/ with /v2/, then send the same privileged-action request. CVE-2025-27507 and both Mealie CVEs show that authorization patches rarely apply retroactively to deprecated versions. The endpoint exists, the server accepts it, and the gate is absent because the fix was applied only to the current version.
The tech_detector spell on intel.mago.team maps allowed methods per endpoint through passive OPTIONS observation during API surface mapping (MAGO team tooling). The output includes, for each discovered endpoint, the Allow header method list and response status codes per tested role.
The API documentation describes what the server does. The methods it omits describe what the server also does, without checking who asked.
Top comments (0)