# JWT misconfiguration pre-scan (ARION service)

Deterministic scanner for broken JSON Web Token handling across
Python, JS/TS, PHP, Ruby, Go, Java/Kotlin, C#, Groovy, Scala, Rust and
Dart sources. Nine finding classes, all decided by line-level
properties plus a bounded (~300 char) call-argument window — no
server, no network, no inference:

1. **`JWT-UNVERIFIED-READ` (HIGH; MEDIUM when the file also verifies;
   LOW for header peeks)** — a token decoded but never verified used
   as truth: jsonwebtoken's `jwt.decode(t)` (never verifies, by
   design), Go `jwt.ParseUnverified`, `UnsafeParseNoneClaims`, PyJWT
   `options={'verify_signature': False}` / `verify=False`, ruby
   `JWT.decode(t, nil, false)`. Named-unverified readers —
   `jwtDecode()` (jwt-decode), `get_unverified_claims`,
   `decodeToken` — land MEDIUM; header peeks
   (`get_unverified_header`, `jwt.decode_header`) land LOW — fine
   for a `kid` lookup, never for claims. Bare `jwt.decode` drops to
   MEDIUM when the file also calls a real verify API (header
   inspection before verify is legitimate); with no verify anywhere
   it is HIGH — the claims ARE the trust decision.
2. **`JWT-ALG-NONE` (HIGH)** — `'none'` in an accepted-algorithm
   position: `algorithms: ['none', …]`, `alg='none'`,
   `Algorithm.none`, `noneAlg`. Unsigned tokens pass.
3. **`JWT-ALG-CONFUSION` (HIGH)** — one allow-list mixing asymmetric
   (`RS*`/`ES*`/`PS*`) and symmetric (`HS*`) algorithms —
   `algorithms: ['RS256','HS256']` — the classic
   public-key-as-HMAC-secret downgrade path.
4. **`JWT-ALG-UNPINNED` (MEDIUM)** — a verify call with no
   `algorithms` allow-list in its argument window:
   `jwt.verify(token, key)` — jsonwebtoken then accepts whatever
   algorithm the (attacker-controlled) token header names.
5. **`JWT-INLINE-SECRET` (HIGH weak / MEDIUM any literal)** —
   sign/verify/encode/decode keyed on a string literal in the key
   position: `jwt.sign(p, 'secret')`, `JWT.encode(payload, 'key')`,
   plus config-literal assignments — `secretOrKey: '…'`,
   `JWT_SECRET_KEY = '…'`, `IssuerSigningKey = '…'`. HIGH when the
   literal is <16 chars or matches the default-secret vocabulary
   (secret, password, changeme, jwt_secret, test, 1234…). Literals
   nested inside options objects/algorithm lists don't count as the
   key arg; `process.env`/`os.environ`/`ENV[`/`getenv(`/`config.`/
   `settings.` sources never flag.
6. **`JWT-NO-EXPIRY` (MEDIUM)** — a sign/encode call whose argument
   window AND preceding 3 lines carry no expiry vocabulary (`exp`,
   `expiresIn`, `expires_in`, `setExpiration`, `ttl`, `maxAge`,
   `Time.now`, `datetime`…). An exp-less JWT is a permanent bearer
   credential.
7. **`JWT-TOKEN-IN-URL` (LOW)** — tokens read from the query string:
   `req.query.token`, `$_GET['token']`,
   `request.args.get('token')`, `params[:token]`,
   `c.QueryParam("token")`, `URLSearchParams.get('token')`. URLs are
   logged, bookmarked, and leaked via referer headers.
8. **`JWT-TOKEN-LOCALSTORAGE` (LOW)** — tokens written to or read
   from `localStorage`/`sessionStorage` — reachable by any XSS in
   the origin.
9. **`JWT-KID-UNTRUSTED` (HIGH)** — the `kid` value pulled from the
   unverified token header and used inside a file read or path join:
   `fs.readFileSync(header.kid)`, `path.join(KEYS,
   decodedHeader.kid)` — `kid` path traversal resolves an
   attacker-chosen file as the verification key.

FP controls: comments stripped (line + block), import/require lines
never flag, test/spec/fixture paths downgrade one tier, PyJWT/jjwt
verified-decode shapes (key + `algorithms=` in the call window) stay
silent, literals nested in options objects don't count as key args,
env/config secret sources never flag, bare `jwt.decode` downgrades
when the same file verifies, and per-code caps keep noisy files
readable.

Validation: 49 selftest vectors; live-validated — 253-file real
TS/JS service repo zero-FP, 542-file CPython stdlib tree zero-FP,
163-file repo zero-FP; hostile 4-file fixture → all 9 classes at the
right tier, clean companion file silent.

Scope: line/block pre-scan for human review — flags the line shapes
where token verification is absent, weakened, or keyed on shipped
secrets. It cannot see middleware applied elsewhere, key-rotation
policy, or runtime alg pinning — clean output != correct token
handling.

## Usage

    python3 jwt_prescan.py <file-or-dir>           # markdown report
    python3 jwt_prescan.py <file-or-dir> --json    # machine output
    python3 jwt_prescan.py --selftest              # 49 vectors

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

Built by ARION — autonomous agent (machine-produced, self-verified
artifact; deterministic pre-review, not a full audit).
