Pro feature — BAC (
bac_audit, Authorize view,/api/bac/*,hugin bacCLI, campaignkind=bac, workflowBacTagCorpus) is gated behindFeature::Bac. Community tier gets a 402 / “Pro license required” message on every surface. Trial, Pro, and Dev-bypass builds have full access.
Broken Access Control is OWASP’s #1 category and the single highest-ROI bug class in every bug bounty program. Hugin ships a full BAC pipeline — passive signal extraction on every captured flow, a corpus-driven active audit, scored findings, and six export formats. This page documents the whole surface: what gets emitted, what gets scored, how the checks work, and how to read the output.
If you just want the mechanical “how do I run it” recipe, skip to Running an Audit. Everything above that explains how each piece of the pipeline feeds the one after it.
🔗Pipeline Overview
every captured flow ──▶ BacPipeline::analyze (passive)
│
├─▶ bac_signals (7 detectors)
├─▶ bac_id_corpus (IDs + types)
└─▶ bac_shapes (response shape hashes)
explicit audit run ───▶ BacActiveEngine (active — 11 checks)
│ reads signals + corpus + shapes
│ replays across identity matrix
└─▶ bac_findings (scored, deduped)
findings ───▶ export renderer (markdown / sarif / html / csv / summary / json)
Every captured proxy flow runs through the passive pipeline automatically. The active audit is an explicit action — clicking Scan in the Authorize view, calling the bac_audit audit MCP action, or POST /api/bac/audit.
🔗Passive: Signals
Seven detectors emit BacSignal rows into bac_signals. Signals are observations, not findings — they tell the active engine what’s worth probing and seed the UI’s Intelligence panel.
| Kind | Detector | Fires when |
|---|---|---|
role_field | RoleFieldDetector | Response body contains a recognised privilege field — role, is_admin, permissions, org_id, tenant_id, owner_id. |
predictable_id | PredictableIdDetector | An ID in the response (path or body) has low entropy — sequential numeric, UUIDv1 with a timestamp leak. |
identity_drift | IdentityDriftDetector | Same endpoint served ~identical bodies both with and without auth headers. |
shape_convergence | ResponseShapeDetector | Two distinct identities produced the same structural response-shape hash on the same endpoint. The strongest automated horizontal-IDOR tell. |
bypass_header_flip | (active-side emitted) | A bypass-header candidate flipped a 401/403 to a 2xx. |
mass_assignment_echo | RoleFieldDetector | A privilege field from the request was reflected in the response. |
saml_assertion | SamlAssertionDetector | Request or response carries SAMLResponse or <saml:Assertion> markup. Detection-only — flags the flow for manual review. |
Signals carry (endpoint, locator, sample, identity_id, confidence) so the active engine can target the exact parameter the detector saw.
🔗Passive: ID Corpus
IdCorpusDetector walks every path segment, query param, and JSON body field of every flow and classifies the ones that look like identifiers. Classifications land in bac_id_corpus:
| Kind | Pattern |
|---|---|
numeric | ^[0-9]+$ (≥ 2 digits — skips 1 / 2 pagination noise) |
uuid_v4 | 8-4-4-4-12 hex, version nibble = 4 |
uuid_v1 | Same shape, version nibble = 1 (timestamp + MAC leak) |
uuid_other | 8-4-4-4-12 hex, any other version |
slug | URL-safe lowercase + hyphen, 4–64 chars, at least one letter |
hex | 8–64 hex chars (hashes, short tokens) |
opaque_token | 12–256 alphanumeric + base64 chars |
The corpus is the vocabulary the active engine rotates during cross-identity and path-param checks. The more flows you capture as different users, the richer the rotation surface — tag flows with a session profile’s identity via the Logger context menu or the session_profiles capture MCP action.
Four producers feed the corpus:
- Proxy passive pipeline — automatic on every captured flow.
- Nerve cross-wire — when Nerve classifies a param as
idor/auth/mass_assign, the value gets written into the corpus too. - OpenAPI spec seeding —
bac_audit seed_corpusparses a spec and inserts everyexample/default/enum[0]value as a corpus entry. - Workflow
BacTagCorpusaction — passive workflows can auto-tag flows matching a trigger.
🔗Passive: Response Shapes
Two flows on the same endpoint with the same structural response shape (key names, value types, array vs scalar) are usually returning the same kind of resource. Two different identities seeing the same shape on the same endpoint is the strongest “authz isn’t enforced per-principal” tell the pipeline emits.
ResponseShapeDetector hashes each JSON body’s structural skeleton, writes it to bac_shapes, and the cross-identity check in the active engine rotates the baseline vs replay through the shape matrix.
🔗Active: The Audit Run
BacActiveEngine::audit consumes passive signals + corpus + shapes and runs up to 11 targeted checks across an identity matrix. Each check is independently toggleable via BacAuditRequest.enable_* / the MCP disable_* flags / the Authorize view’s advanced toggles.
| Check | What it does | Finding kind |
|---|---|---|
path_param | Rotates ID-like URL path segments against the corpus. | PathParamMutation |
param_mutation | Rotates query and body ID params. Closes the classic BOLA gap. | ParamMutation |
cross_tenant | Rotates tenant / org / owner fields against values seen under other identities. | CrossTenantAccess |
bypass_header | Fires 60 well-known bypass headers (X-Original-URL, X-Forwarded-For localhost, etc.) against 401/403 responses. | BypassHeader |
jwt_escalation | Forges alg=none tokens and role/scope claim mutations. | JwtEscalation |
graphql_introspect | Probes for introspection-enabled endpoints and sensitive field exposure. | (surfaces via scanner findings) |
cross_identity_echo | Flags endpoints where two distinct identities receive near-identical response bodies. | CrossIdentityEcho |
rate_limit_bypass | Swaps auth headers on a rate-limited endpoint — 429 → 2xx means the counter is session-keyed, not IP-keyed. | RateLimitBypass |
role_enum | Enumerates role values known from corpus (e.g. admin, superuser) on role-field params. | RoleEscalation |
oauth_scope | Sends an elevated scope= on OAuth /token endpoints — if the issued token reflects the requested scope, the server trusts client-supplied scope. | OAuthScopeEscalation |
mass_assignment | Sends privileged fields (is_admin=true, role=admin) in request bodies. | MassAssignment |
Audit lifecycle is registered in a shared AUDIT_REGISTRY so MCP, REST, UI, and CLI callers all see the same cancel flag + progress atomics for a given audit_id:
- Start:
bac_audit audit(MCP) /POST /api/bac/audit(REST) / Scan button (UI) - Progress:
bac_audit progress(MCP) /POST /api/bac/audit/progress(REST) - Cancel:
bac_audit cancel(MCP) /POST /api/bac/audit/cancel(REST) / Abort button (UI)
A UI-initiated audit is observable from an MCP agent in real time and cancellable from a curl — the lifecycle unifies across surfaces.
🔗Findings
Twelve BacFindingKind variants, persisted in bac_findings with a per-kind dedup_key so the same underlying issue detected twice doesn’t produce duplicate rows.
| Kind | Severity default | Typical CWE |
|---|---|---|
StatusDivergence | Medium | CWE-285 |
CrossIdentityEcho | High | CWE-639 |
BypassHeader | High | CWE-284 |
PathParamMutation | High | CWE-639 |
ParamMutation | High | CWE-639 |
CrossTenantAccess | Critical | CWE-639 |
MassAssignment | High | CWE-915 |
JwtEscalation | Critical | CWE-287 |
AuthOptionalLeak | Medium | CWE-200 |
RateLimitBypass | Medium | CWE-770 |
RoleEscalation | High | CWE-269 |
OAuthScopeEscalation | Critical | CWE-269 |
Every finding carries baseline_flow_id + replay_flow_id so the UI can offer per-row handoffs (Repeater, Intruder, Comparer, Sitemap — see the Authorize view doc) and evidence with a short human-readable summary.
🔗Running an Audit
🔗From the UI
- Open Authorize (left sidebar, only visible on Pro/Trial/Dev).
- Create at least two Session Profiles — mark one as baseline (usually admin), capture the other(s) from live traffic via the Capture from flow button.
- Click Scan. Progress bar shows
flows_processed / total_flows; Abort button cancels. - Findings land in the Findings sub-tab; signals + corpus + shapes land in Intelligence.
🔗From MCP
// 1. Start
{ "tool": "bac_audit", "action": "audit",
"identity_ids": ["baseline-id", "user-b-id"] }
// → { "audit_id": "e5c…", "findings": [...] }
// 2. Poll progress
{ "tool": "bac_audit", "action": "progress", "audit_id": "e5c…" }
// 3. Cancel
{ "tool": "bac_audit", "action": "cancel", "audit_id": "e5c…" }
// 4. Export
{ "tool": "bac_audit", "action": "export", "format": "sarif" }🔗From REST
# Start — returns audit_id you can watch
curl -X POST http://localhost:8081/api/bac/audit \
-H 'Content-Type: application/json' \
-d '{"identity_ids":["p1","p2"]}'
# Progress — poll with the returned audit_id
curl -X POST http://localhost:8081/api/bac/audit/progress \
-H 'Content-Type: application/json' \
-d '{"audit_id":"e5c…"}'
# Cancel
curl -X POST http://localhost:8081/api/bac/audit/cancel \
-H 'Content-Type: application/json' \
-d '{"audit_id":"e5c…"}'🔗From CLI (read-only)
The CLI covers the non-audit side — no AppState / proxy / MCP needed, just the store:
hugin bac findings --severity high --format json
hugin bac signals --kind role_field --limit 50
hugin bac corpus --kind uuid_v4
hugin bac export --format sarif > findings.sarif
hugin bac purge --project-id <uuid>
Audit itself needs AppState and lives behind the MCP / REST / UI surfaces.
🔗Scheduling a Nightly Audit
Campaigns support kind: "bac" (Pro-only). Create a scheduled campaign via the Campaigns view or POST /api/campaigns:
{
"name": "Nightly BAC sweep",
"description": "{\"kind\":\"bac\",\"schedule\":\"0 2 * * *\",\"bac_config\":{\"request_budget\":10000}}"
}
The scheduler loop in hugin-api hits the audit every time the cron fires, updates next_run_at, and respects the per-concurrency semaphore so BAC campaigns don’t stack.
🔗Seeding the Corpus from an OpenAPI Spec
If you have an OpenAPI / Swagger spec with concrete example values for path / query params, pre-seed the corpus so the first audit has real rotation vocabulary before any proxy traffic has been captured:
curl -X POST http://localhost:8081/api/mcp/bac_audit \
-H 'Content-Type: application/json' \
-d '{"action":"seed_corpus","spec":"'"$(cat openapi.json | jq -Rs .)"'"}'
Or from an MCP agent:
{ "tool": "bac_audit", "action": "seed_corpus",
"spec": "<raw OpenAPI JSON body>",
"project_id": "<optional>",
"identity_id": "<optional>" }🔗Exporting Findings
Six formats, all accepted by both bac_audit export (MCP) and hugin bac export (CLI):
markdown— bug-bounty-ready per-finding writeupssarif— SARIF 2.1.0, GitHub Code Scanning compatiblehtml— self-contained standalone reportcsv— row-per-finding, importable into Excel / sheetssummary— severity counts + totals, JSONjson— raw findings array (same shapeget_findingsreturns)
🔗See Also
- Authorize View — the UI surface for managing session profiles, running audits, and reading findings
- Authorization Scanner — the classic Autorize matrix scanner (stateless, inline results). BAC’s active audit is the superset.
- Nerve — parameter classifier that cross-wires
idor/auth/mass_assignhits into the BAC corpus - MCP Tools — full parameter reference for
bac_audit,session_profiles,live_audit