BAC — Broken Access Control Pipeline

Pro feature — BAC (bac_audit, Authorize view, /api/bac/*, hugin bac CLI, campaign kind=bac, workflow BacTagCorpus) is gated behind Feature::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.

KindDetectorFires when
role_fieldRoleFieldDetectorResponse body contains a recognised privilege field — role, is_admin, permissions, org_id, tenant_id, owner_id.
predictable_idPredictableIdDetectorAn ID in the response (path or body) has low entropy — sequential numeric, UUIDv1 with a timestamp leak.
identity_driftIdentityDriftDetectorSame endpoint served ~identical bodies both with and without auth headers.
shape_convergenceResponseShapeDetectorTwo 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_echoRoleFieldDetectorA privilege field from the request was reflected in the response.
saml_assertionSamlAssertionDetectorRequest 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:

KindPattern
numeric^[0-9]+$ (≥ 2 digits — skips 1 / 2 pagination noise)
uuid_v48-4-4-4-12 hex, version nibble = 4
uuid_v1Same shape, version nibble = 1 (timestamp + MAC leak)
uuid_other8-4-4-4-12 hex, any other version
slugURL-safe lowercase + hyphen, 4–64 chars, at least one letter
hex8–64 hex chars (hashes, short tokens)
opaque_token12–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:

  1. Proxy passive pipeline — automatic on every captured flow.
  2. Nerve cross-wire — when Nerve classifies a param as idor / auth / mass_assign, the value gets written into the corpus too.
  3. OpenAPI spec seedingbac_audit seed_corpus parses a spec and inserts every example / default / enum[0] value as a corpus entry.
  4. Workflow BacTagCorpus action — 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.

CheckWhat it doesFinding kind
path_paramRotates ID-like URL path segments against the corpus.PathParamMutation
param_mutationRotates query and body ID params. Closes the classic BOLA gap.ParamMutation
cross_tenantRotates tenant / org / owner fields against values seen under other identities.CrossTenantAccess
bypass_headerFires 60 well-known bypass headers (X-Original-URL, X-Forwarded-For localhost, etc.) against 401/403 responses.BypassHeader
jwt_escalationForges alg=none tokens and role/scope claim mutations.JwtEscalation
graphql_introspectProbes for introspection-enabled endpoints and sensitive field exposure.(surfaces via scanner findings)
cross_identity_echoFlags endpoints where two distinct identities receive near-identical response bodies.CrossIdentityEcho
rate_limit_bypassSwaps auth headers on a rate-limited endpoint — 429 → 2xx means the counter is session-keyed, not IP-keyed.RateLimitBypass
role_enumEnumerates role values known from corpus (e.g. admin, superuser) on role-field params.RoleEscalation
oauth_scopeSends an elevated scope= on OAuth /token endpoints — if the issued token reflects the requested scope, the server trusts client-supplied scope.OAuthScopeEscalation
mass_assignmentSends 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.

KindSeverity defaultTypical CWE
StatusDivergenceMediumCWE-285
CrossIdentityEchoHighCWE-639
BypassHeaderHighCWE-284
PathParamMutationHighCWE-639
ParamMutationHighCWE-639
CrossTenantAccessCriticalCWE-639
MassAssignmentHighCWE-915
JwtEscalationCriticalCWE-287
AuthOptionalLeakMediumCWE-200
RateLimitBypassMediumCWE-770
RoleEscalationHighCWE-269
OAuthScopeEscalationCriticalCWE-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

  1. Open Authorize (left sidebar, only visible on Pro/Trial/Dev).
  2. 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.
  3. Click Scan. Progress bar shows flows_processed / total_flows; Abort button cancels.
  4. 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 writeups
  • sarif — SARIF 2.1.0, GitHub Code Scanning compatible
  • html — self-contained standalone report
  • csv — row-per-finding, importable into Excel / sheets
  • summary — severity counts + totals, JSON
  • json — raw findings array (same shape get_findings returns)

🔗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_assign hits into the BAC corpus
  • MCP Tools — full parameter reference for bac_audit, session_profiles, live_audit