Skip to content

People roster endpoint leaks the company directory to profile-missing (degraded) sessions

Why (Accounting (Diagnostics), 2026-06-29)

Found while reviewing the Codex commit batch (owner asked to fix overlooked bugs). GET /api/people/roster returns the addressable-people directory (names, emails, roles, subsidiaries). Its auth gate could be passed by a session whose RBAC profile failed to load.

Diagnosis

The endpoint gated only on isAccessAllowed(session.user.role, session.user.status).

When the server-side RBAC profile read fails, auth fails closed by setting user.profileMissing = true β€” but the degraded session is left as role = 'pending', status = 'active' (documented in T-073). isAccessAllowed returns true for ('pending', 'active') (it only rejects on missing role/ status or non-active status), so a profile-missing session sailed through and could scrape the roster.

Every other protected surface already rejects this state: lib/api/withSubsidiaryAuth.ts:88, lib/rbac/serverAuth.ts:101, lib/rbac/serverAuthApp.ts:83 all gate on session.user.profileMissing. The roster endpoint was the outlier.

What shipped

pages/api/people/roster.ts β€” the 403 gate now also rejects when session.user.profileMissing is set, matching the other protected endpoints:

if (
  session.user.profileMissing ||
  !isAccessAllowed(session.user.role ?? undefined, session.user.status ?? undefined)
) {
  return res.status(403).json({ error: 'Forbidden' })
}

Verification

  • βœ… Attestation (Accounting (Diagnostics)): read AGENTS.md; checked the board by scope, not UID β€” no task covered the roster endpoint's missing profileMissing gate. Tracking T-139.
  • Source: Accounting (Diagnostics) Β· https://claude.ai/code/session_01G58Y71noihrYCDEDMexmea
  • NODE_OPTIONS=--max-old-space-size=4096 npx tsc --noEmit clean; npx vitest run green.

Follow-up (not done β€” flagged for owner)

The deeper question is whether isAccessAllowed should reject role === 'pending' outright, which would close this class of gap for every caller at the source. That is a system-wide RBAC behavior change (it touches onboarding flows), so it is intentionally left for a dedicated, owner-reviewed change rather than bundled here.

Blast Radius

Single endpoint auth gate. No data-shape or roster-content change; the only behavioral change is that degraded/profile-missing sessions now get 403 from the roster endpoint, consistent with the rest of the protected API surface.

Commit index (backfilled 2026-07-01, best-effort Β· Coaching (Diagnostic))

Candidate related commits, auto-backfilled from git on main: commits whose message references this task's UID or a PR number it cites. Not verified β€” this squash-merged history can't yield a precise per-task list, so rows tagged (mentions only) name the task in passing (may be tangential) and untagged work commits may be missing. Treat as a starting point: verify, prune tangential rows, and append any real ones per the AGENTS.md "record every related SHA" policy.

  • (no git-discoverable commit references this UID or its PRs β€” append real SHAs here as identified.)