Repository navigation
Description
Problem
pm-dispatch's OIDC verification assumes the decoded JWT header and payload are JSON objects (pm-stack/dispatch/src/auth.ts, main 732ca26), but parseJwt (auth.ts:53-67) accepts any JSON value: JSON.parse of the base64url-decoded first segment can be null, a number, a string, or an array, and the result is cast straight into JwtParts.header/payload typed as Record<string, unknown>.
verifyOidcToken then dereferences it before any check can run:
if (jwt.header.alg !== "RS256") throw new AuthError("oidc: unexpected alg (want RS256)"); // auth.ts:101For a token whose header decodes to JSON null, that line throws a raw TypeError, not an AuthError. The server's error mapping (src/server.ts handler catch) turns AuthError into 401 but everything else into 500 — so a malformed credential, the most routine bad input an auth endpoint receives (truncated tokens, non-GitHub JWTs, scanner probes), is answered as a server fault instead of an authentication failure. (The payload segment has the same shape of hole at auth.ts:129-131, reachable after a signature passes.)
Evidence (verified this run)
Laptop probe (bun, importing the real auth.ts from the laptop primary at main 732ca26, md5-identical to the sandbox clone; probe at ~/git/fleet/probe-2026-10-30-laneB-dispatch/ on the laptop), token = base64url("null") + "." + base64url({iss:…}) + "." + base64url("sig"):
D parseJwt(null header): null
D verify throws: TypeError - null is not an object (evaluating 'jwt.header.alg') | status= undefined
An AuthError carries status = 401; this throw carries none. Control: a well-formed header with an unknown kid correctly throws AuthError ("signing key not found in JWKS").
Why it matters
Auth endpoints should fail closed and fail classified: a 500 for a bad credential pollutes error monitoring with phantom server faults, can page like an outage, and hides real 500s in the noise. It also leaks an implementation detail (the service 500s on null headers but 401s on other bad tokens) to unauthenticated callers probing the endpoint.
Acceptance criteria
-
parseJwtrejects any header/payload segment that does not decode to a plain JSON object (returns null → "malformed JWT"AuthError), coveringnull, arrays, and scalar JSON. - Every malformed-token input to
verifyOidcToken/authenticatesurfaces asAuthError(HTTP 401), never a rawTypeError/500. - Tests pin the null-header, array-payload, and scalar-segment cases alongside the existing auth tests.
- Existing dispatch tests still pass.
Activity
dezren39 commentedon Oct 30, 2026
🤖 Agent note · filed
dezren39 commentedon Oct 30, 2026
agent-note:v1 — design-architect adoption (pass 19, 2026-10-30): this issue is Phase 1 (boundary) of design #2668, "pm-dispatch launch pipeline, staged", together with #2664. Design doc:
files/designs/code-pm-dispatch-launch-pipeline.mdin the keep-the-four-repos-healthy goal workspace.Recorded decision for triage (do not re-file, re-scope, or split this issue; record the phase structure instead): the fix belongs at parse time —
parseJwtreturnsnullwhen the header or payload decodes to a non-plain-object JSON value (null, array, or scalar; arrays must be excluded explicitly,typeof [] === "object"), so malformed credentials take the existingAuthError→ 401 path. Do NOT instead mapTypeError→ 401 in the server handler: that papers over one crash site and leaves the post-signature payload hole (auth.ts:129-131) open.This issue already carries a queued triage packet; no new packet was enqueued by the architect.
13 remaining items
dezren39 commentedon Nov 2, 2026
🤖 Agent note · done
queue/5422-2665-reject-non-object-jwt-header-payload; PR fix(pm-dispatch): reject non-object JWT segments #4241; packet task 5422dezren39 commentedon Nov 3, 2026
🤖 Agent note · done
user/reviewlabel: diff is exactly the parse-boundary gate plus regression tests; Codex review reported no findings; targeted tests verified on the laptop at PR head ba53f5a (auth.test.ts 17/17 pass; server.test.ts 7/7 on re-run — the one full-suite timeout was the slow fix(pm-dispatch): POST /v1/sessions accepts an arbitrary directory — OIDC callers escape their repo clone #2903 sessions test, which passes standalone on both the branch and main). The PR's failing pm-aggregator check also fails on main throughout 2026-11-03 (pre-existing, unrelated Rust stack). Merged PR fix(pm-dispatch): reject non-object JWT segments #4241 (squash, merge commit 115fe4f); this issue auto-closed via "Fixes pm-dispatch(auth): a JWT whose header decodes to non-object JSON (null/array/scalar) throws a raw TypeError — malformed credentials get HTTP 500, not 401 #2665".developing-today commentedon Nov 3, 2026
agent-note:v1
state: triaged (coverage pass)
Coverage pass 2026-11-03: reviewed in the fleet-wide per-issue pass (drift / overlap / already-handled / stale / blockers / ordering). Verdict: still live; queued in repo-health-code-pilot as triage at priority 30 (priority/low). If this is already handled at current main, the triage closes it with evidence; if it needs splitting, the triage files the split.
Found via: coverage pass 2026-11-03