Skip to content

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

Closed

Description

@dezren39

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:101

For 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

  • parseJwt rejects any header/payload segment that does not decode to a plain JSON object (returns null → "malformed JWT" AuthError), covering null, arrays, and scalar JSON.
  • Every malformed-token input to verifyOidcToken/authenticate surfaces as AuthError (HTTP 401), never a raw TypeError/500.
  • Tests pin the null-header, array-payload, and scalar-segment cases alongside the existing auth tests.
  • Existing dispatch tests still pass.

Activity

  1. added
    kind/bugSomething isn't working (prefixed form of the classic bug label)
    task/triageQueue packet kind: verify, reproduce, and route this item
    source/creatorFiled by the new-issue-creator job
    priority/lowNice to have; do when convenient
    queue/queuedHas a queued task in the Supabase agent queue
    effort/xsTrivial: expected under 30 minutes of work
    app/pm-stackpm-stack (Pad/Grist) in developing-today/code
    on Oct 30, 2026
  2. dezren39 commented on Oct 30, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · filed

    • Agent: new-issue-creator (lane B, new-issue-creator-code-b)
    • Session / ID: new-issue-creator-code-b run 2026-10-30 04:20 CDT
    • Found via: filed this run
    • Task: Triage pm-dispatch's JWT shape assumption (auth.ts parseJwt/verifyOidcToken): pin that non-object header/payload segments are malformed-token AuthErrors (401), then route a small_fix packet.
    • Expectation: effort/xs — done looks like the acceptance criteria on this issue being pinned and a fix packet routed.
    • Links: evidence in the issue body (file:line at main 732ca26 + laptop probe ~/git/fleet/probe-2026-10-30-laneB-dispatch/).
  3. dezren39 commented on Oct 30, 2026

    @dezren39
    MemberAuthor

    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.md in 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 — parseJwt returns null when 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 existing AuthError → 401 path. Do NOT instead map TypeError → 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.

  4. 13 remaining items

  5. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · done

  6. dezren39 commented on Nov 3, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · done

  7. added
    queue/queuedHas a queued task in the Supabase agent queue
    and removed
    queue/claimedQueue worker lifecycle state
    work/assignedQueue worker lifecycle state
    on Nov 3, 2026
  8. developing-today commented on Nov 3, 2026

    @developing-today
    Contributor

    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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

app/pm-stackpm-stack (Pad/Grist) in developing-today/codeeffort/xsTrivial: expected under 30 minutes of workkind/bugSomething isn't working (prefixed form of the classic bug label)priority/lowNice to have; do when convenientqueue/queuedHas a queued task in the Supabase agent queuesource/creatorFiled by the new-issue-creator jobsource/design-architectFiled by the repo design architect jobstatus/needs-triageNot yet triaged; needs a first looktask/triageQueue packet kind: verify, reproduce, and route this item

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions