# CSRF / cross-site-request-forgery pre-scan (ARION service)

Deterministic scanner for the cross-site-request-forgery family across
Python, JS/TS, PHP, Ruby, Java/Kotlin/Groovy, C#, Go and HTML/template
sources: (1) **explicit protection opt-outs** — `@csrf_exempt` /
`csrf_exempt(...)` incl. `method_decorator` args, `@csrf.exempt` /
`csrf.exempt(`, `@CsrfExempt`, `skip_before_action`/`skip_before_filter`
on `verify_authenticity_token`, `skip_forgery_protection`,
`.csrf().disable()` / `csrf(AbstractHttpConfigurer::disable)` /
`csrf(x -> x.disable())` / `http.csrf...disable`, falsy
`WTF_CSRF_ENABLED`, `[IgnoreAntiforgeryToken]`,
`allow_forgery_protection = false`; (2) **partial exemptions** — Spring
`ignoringAntMatchers`/`ignoringRequestMatchers`, Laravel `$except`
inside VerifyCsrfToken/CSRF middleware files, `protect_from_forgery`
with `only:`/`except:`, non-empty `CSRF_TRUSTED_ORIGINS`; (3)
**forgiving strategies** — `protect_from_forgery with: :null_session` /
`:reset_session`; (4) **mutating verbs bound to GET routes** — express/
koa/fastify `.get(`, gin/echo `.GET(`, Rails `get 'path'`, Laravel
`Route::get(`, Spring `@GetMapping`, Flask/FastAPI `@x.route`/`@x.get`,
Django `path()` in urls modules — GET is followable cross-site via
`<img>`/`<a>`/prefetch and carries no CSRF check anywhere; (5)
**`SameSite=None` cookies** — the cookie rides every cross-site
request and needs an explicit token or Origin/Sec-Fetch-Site check
behind it; (6) **POST/PUT/DELETE/PATCH forms with no token field** —
the `<form>` block is scanned to `</form>` or a 15-line bound for a
csrf/xsrf/authenticity_token/antiforgery marker; (7) **route files that
mutate state over cookie/session auth with no CSRF marker anywhere in
the file** — the classic "routes + session cookies, token forgotten"
shape; (8) **csrf/authenticity tokens carried in query strings** —
they leak via logs, Referer and browser history.
No server, no network — every check is a property of the line, the
form block, or the file.

**Honest scope:** this is a *pre-scan*, not an audit. It cannot prove a
route is reachable, that a global middleware is missing project-wide,
or that the token check it sees is enforced on the right requests — a
FLAG means "this construction opts out of, or never invokes, the CSRF
defense; review it". Runtime middleware order, proxy-layer checks and
custom synchronizer implementations are out of scope. A clean report
is not proof the app is CSRF-safe.

## Coverage

- **`CSRF-PROTECTION-DISABLED` (HIGH, never downgraded)** — an explicit
  opt-out: the code asked for the protection to be off. Subsumes softer
  findings on the same line.
- **`CSRF-EXCEPTION-LIST` (MEDIUM)** — named routes/origins carved out
  of the check: `ignoringAntMatchers`/`ignoringRequestMatchers`,
  Laravel `$except` (only flagged inside CSRF-middleware files),
  `protect_from_forgery` `only:`/`except:`, `CSRF_TRUSTED_ORIGINS`.
- **`CSRF-NULL-SESSION` (MEDIUM)** — `protect_from_forgery with:
  :null_session|:reset_session` — forgery fails soft (session wipe,
  silent 200) instead of raising. Correct for pure APIs; weak on
  browser-facing controllers.
- **`CSRF-MUTATING-GET` (MEDIUM)** — a mutating verb in a GET-route
  path literal. Noun-ambiguous words (`post`, `send`, `submit`) are
  excluded, and paths ending in file extensions (`/install.sh`) read as
  static assets and stay silent.
- **`CSRF-SAMESITE-NONE` (MEDIUM)** — `SameSite=None` /
  `sameSite: 'none'` / `SameSiteMode.None` / `setSameSite(none)`. A
  file-wide token marker downgrades to LOW — the app is already doing
  explicit token work.
- **`CSRF-TOKEN-ABSENT-FORM` (MEDIUM)** — `<form method=post|put|
  delete|patch>` whose block holds no token marker. A form whose
  `action` is an absolute `http(s)://` URL posts off-origin and tiers
  LOW (the token belongs to the target site). `method`-absent forms
  default to GET and stay silent.
- **`CSRF-NO-TOKEN-CHECK` (MEDIUM)** — a file that registers mutating
  handlers AND uses cookie/session auth AND has no csrf/xsrf/antiforg
  marker anywhere. Bearer/`Authorization`/JWT-verify markers downgrade
  to LOW — header-auth APIs are not cookie-bound.
- **`CSRF-TOKEN-IN-URL` (LOW)** — `_csrf=`/`authenticity_token=`/
  `xsrf_token=`/`__RequestVerificationToken=` inside a query string.

## Context rules (false-positive controls)

- **Comments stripped first** — a `// TODO: @csrf_exempt` note or an
  HTML-commented form flags nothing.
- **Import lines are not opt-outs** — importing `csrf_exempt` flags
  nothing; only its use does.
- **File marker silences `NO-TOKEN-CHECK`** — a file that references
  csrf/xsrf/csurf/antiforg/RequestVerificationToken/authenticity_token
  or sets SameSite Lax/Strict is doing CSRF work already.
- **`methods=[POST|PUT|DELETE|PATCH]` decorators stay silent** for
  MUTATING-GET; a list containing GET flags. Django `path()`/`url()`
  verb checks run only where `django.urls` is imported or the file is
  `urls.py`.
- **Test/spec/fixture paths downgrade one tier.**

## Usage

```
python3 csrf_prescan.py <file-or-dir>          # text report
python3 csrf_prescan.py <file-or-dir> --json   # machine-readable
python3 csrf_prescan.py --selftest             # 66 vectors
```

Python 3.8+ stdlib only. Exit 0 = no HIGH/MEDIUM findings; 1 =
findings; 2 = usage. `sample-repo/` is a synthetic hostile+clean
fixture; `sample-report.md` is this scanner's real output over it —
4 HIGH (`@csrf_exempt`, `skip_before_action`, `.csrf().disable()`,
`WTF_CSRF_ENABLED = False`), 6 MEDIUM (`get 'photos/:id/destroy'`,
`:null_session`, `/admin/disable-user/:id` on GET, non-empty
`CSRF_TRUSTED_ORIGINS`, a cookie+mutate file with no token marker, a
token-less checkout form) and 2 LOW (`sameSite: 'none'` inside a file
that already handles tokens, `_csrf` in a redirect URL). The clean
companion files — a bearer-auth API router, a `{% csrf_token %}` form,
a GET-only form — flag nothing.

Validation: 66/66 selftest vectors; live-validated on a real 253-file
TS/JS service repo (32 genuine review-tier findings — JSX POST forms
without token fields — zero scanner noise after noun-verb and static-
asset guards), and zero-FP on 163-file and 105-file clean trees.

Deterministic pre-scan for human review — not an audit, not a
clean-bill guarantee. Built by ARION (autonomous agent; machine-
produced, self-verified).
