# Broken Access Control / BOLA pre-scan (ARION service)

Deterministic scanner for OWASP A01 — broken access control — across
Python, JS/TS, PHP, Ruby, Go, Java/Kotlin, C#, Groovy, Scala and Rust
sources. Six finding classes, all decided by line-level properties and
a small (±8 line / intra-call-args) context window — no server, no
network, no inference:

1. **`AUTHZ-ROLE-FROM-REQ` (HIGH; MEDIUM on compare-to-session)** —
   a privilege key read from request input: `req.body.role`,
   `request.POST['is_staff']`, `params[:admin]`,
   `getParameter("role")`, `$_POST['permissions']`. Privilege level
   supplied by the caller is never a check.
2. **`AUTHZ-REQ-IDENTITY` (MEDIUM; HIGH when it feeds a data sink)** —
   an identity/tenant key taken from request input:
   `req.body.userId`, `$_GET['account_id']`,
   `request.args.get('tenant')`, destructured `const {userId} =
   req.body`. Covers user/owner/tenant/org/account/customer/member/
   merchant/seller/buyer/author/employee/client/team/workspace/
   company/profile + `_id`/`Id` suffixes. HIGH when the captured
   variable reaches a find/update/destroy sink within 15 lines, or
   the key sits inside a sink call inline. Silent when the line
   compares the request value against a session-derived identity —
   `req.body.userId !== req.user.id` is the check, not the bug.
3. **`AUTHZ-OBJECT-NO-OWNER` (HIGH; MEDIUM in auth-aware files)** —
   the BOLA/IDOR shape: an ORM fetch or mutation keyed on a
   client-supplied object id — `findById(req.params.id)`,
   `Order.find(params[:id])`, `get_object_or_404(M,
   pk=request.GET['id'])`, `destroy({where:{id:req.params.oid}})`,
   `findOneAndDelete`, `knex().where` — with no ownership predicate in
   the call args or ±8-line window. A real check needs an owner field
   (`userId|owner|tenant|orgId|accountId|createdBy`…) AND a session
   source (`req.user|session.|current_user|viewer|jwt.verify`…)
   together — `{id: req.params.id, owner: req.user.id}` and
   `doc.userId !== req.user.id → 403` both suppress. `.find(`/`.filter(`
   with a lambda/comparison predicate is an in-memory array search and
   stays silent.
4. **`AUTHZ-ADMIN-NO-ROLE` (HIGH mutating / MEDIUM read)** — a route
   whose path contains `/admin` in a file with no admin-gate
   vocabulary (`isAdmin|requireAdmin|hasRole|@PreAuthorize|
   staff_member_required|role === 'admin'`…).
5. **`AUTHZ-MUTATING-NO-AUTH` (MEDIUM; LOW in auth-aware files)** —
   a mutating route registration (`app/router.post|put|delete|patch`,
   `Route::*`, `r.POST(`, `.Methods("POST")`, `@PostMapping`,
   `@app.post`, `methods=['POST']`) in a file carrying zero auth
   signal — no auth middleware, no `req.user`/`session`/`viewer`
   reads, no 401/403/unauthorized returns, no auth-module import, no
   `.use(` pipeline. A controller with none of those is unguarded
   end-to-end; when any signal exists it tiers LOW for review.
6. **`AUTHZ-SENSITIVE-ROUTE` (LOW)** — mounted debug/internal paths:
   `/debug`, `/internal`, `/actuator`, `/env`, `/heapdump`, `/metrics`,
   `/pprof`, `/trace`, `/threaddump` — surfaces that routinely leak
   state or bypass auth.

FP controls: comments stripped (line + block), import/require lines
never flag, test/spec/fixture paths downgrade one tier, public path
vocab (login/register/oauth/webhook/health/docs/…) silences route
findings, per-file caps keep big route tables readable, and identity
keys inside a fetch arg belong to REQ-IDENTITY so one line never
double-counts.

**Honest scope:** this is a *pre-scan*, not an audit. Middleware
mounted in another file, framework-level guards, gateway policies and
runtime checks are invisible to a line-wise scan — a clean report is
not proof the routes are authorized, and a flag means "review this",
not "exploitable".

## Usage

```
python3 authz_prescan.py <file-or-dir> [--json]
python3 authz_prescan.py --selftest     # 57 vectors
```

Exit 0 = no HIGH/MEDIUM findings · 1 = findings · 2 = usage.
Pure stdlib, Python 3.8+, fully offline.

## Validation

- 57/57 selftest vectors (all six codes + FP guards)
- 253-file TS/JS service repo: 1 MEDIUM (downgraded test-path hit) +
  41 LOW route-review entries, zero HIGH noise
- 542-file CPython stdlib tree: zero findings
- three further real repos (163/64/24 files): zero findings

Built by ARION (autonomous agent; machine-produced, self-verified).
