Skip to content

Security review (2026-05-11): SEC-15 high PII leak on public pilot list, fixed inline#162

Merged
pokle merged 2 commits into
masterfrom
claude/friendly-fermi-UNaes
May 15, 2026
Merged

Security review (2026-05-11): SEC-15 high PII leak on public pilot list, fixed inline#162
pokle merged 2 commits into
masterfrom
claude/friendly-fermi-UNaes

Conversation

@pokle
Copy link
Copy Markdown
Owner

@pokle pokle commented May 11, 2026

Summary

Whole-repo security re-review for 2026-05-11. bun audit clean. SEC-10 / SEC-11 / SEC-12 fixes from the prior round all hold. One new High finding surfaced and was fixed inline:

  • SEC-15 (High, fixed inline)GET /api/comp/:comp_id/pilot used optionalAuth and only gated on comp.test, so anonymous callers received the full pilot row for every registered pilot in every public comp — including admin-entered email, the linked Better Auth account email (linked_email), and driver_contact (the emergency-contact phone). The route still returns names, classes, teams, glider, and national-body IDs (intentionally public on comp results pages), but the three PII fields are now redacted to null for any caller who is not a comp_admin of that comp.

Two prior-round scope gaps closed without code changes (auditing surfaced no issues):

  • Per-tool MCP auth propagation — every tool under web/workers/mcp-api/src/tools/*.ts forwards apiKey via compApi(env, apiKey, …) / compApiRaw(env, apiKey, …). No tool forges identity or short-circuits to admin.
  • wrangler.toml binding cross-environment audit — all four workers point at single canonical prod IDs. Auth-api and competition-api intentionally share D1.

New scope gap recorded for the next round: systematically audit every remaining optionalAuth route for PII-on-public-read patterns (same class as SEC-15).

Full per-finding status table, walk-through, and methodology appended to docs/security-review.md.

SEC-15 details

What was the bypass. The route declared optionalAuth (not requireAuth), and the only auth check was an early-return for test comps. For all non-test comps, anonymous callers received the full serializeCompPilot output including:

  • email — admin-entered per-pilot email
  • linked_email — Better Auth account email of the linked user (via LEFT JOIN "user" u ON p.user_id = u.id)
  • driver_contact — free-form emergency-contact phone / WhatsApp handle

The frontend even surfaced linked_email as a hover tooltip on the link-status badge for every viewer.

How the fix closes it. A new serializeCompPilotPublic helper wraps serializeCompPilot and nulls out the three PII fields. The GET handler resolves comp_admin membership once and dispatches between the two serialisers. The response shape is unchanged (same key set), so the frontend CompPilot interface and its null-handling fall-back paths both work without modification.

const isAdmin = user
  ? !!(await c.env.DB.prepare(
      "SELECT 1 FROM comp_admin WHERE comp_id = ? AND user_id = ?"
    ).bind(compId, user.id).first())
  : false;
if (comp.test && !isAdmin) return c.json({ error: "Not found" }, 404);

const serialize = isAdmin ? serializeCompPilot : serializeCompPilotPublic;
return c.json({ pilots: pilots.results.map((p) => serialize(alphabet, p)) });

Regression tests that prove it stays closed (new, in web/workers/competition-api/test/pilot-crud.test.ts):

  1. Anonymous caller sees email = linked_email = driver_contact = null, but name / civl_id / linked populated. Seeds a pilot row with user-3 so the LEFT JOIN "user" populates linked_email server-side and the public path is forced to zero it.
  2. Admin caller (regression sanity) — email and driver_contact still populated end-to-end.
  3. Authenticated non-admin caller (user-2) gets the same redacted view as anonymous — being signed in does not grant admin.

All 229 competition-api tests pass (+3 from this PR).

Test plan

  • bun run typecheck:all — all 6 workspaces pass
  • bun run test:all — competition-api 229/229, mcp-api 21/21, engine + root suites pass
  • bun audit — 0 vulnerabilities
  • On deploy: curl -s https://glidecomp.com/api/comp/<some-public-comp>/pilot | jq '.pilots[0] | {email, linked_email, driver_contact}' should be all-null
  • On deploy: re-verify the SEC-10 fix on a live endpoint (X-Glidecomp-Internal-User should still yield 401)

https://claude.ai/code/session_01HEtzn7C9m7QaRyGbUXwfCW


Generated by Claude Code

claude and others added 2 commits May 11, 2026 02:16
SEC-15 (High): GET /api/comp/:comp_id/pilot used optionalAuth and only
gated on comp.test, so anonymous callers received the full pilot row for
every registered pilot in every public comp — including admin-entered
email, the linked Better Auth account email, and driver_contact (the
emergency-contact phone number). The route still returns names, classes,
teams, glider, and national-body IDs (intentionally public on comp
results pages), but the three PII fields are now redacted to null for
any caller who is not a comp_admin of that comp.

Implementation: new `serializeCompPilotPublic` wraps the existing
`serializeCompPilot` and nulls out email / linked_email / driver_contact.
The GET handler resolves comp_admin membership once and dispatches
between the two serialisers. Four new regression tests cover: anonymous
caller (worst case — seeded linked_email path), authenticated-non-admin
caller, admin caller (sanity check that PII still surfaces for admin
flows), and the existing admin happy-path test.

Also appends the 2026-05-11 review round to docs/security-review.md:
walks every prior SEC-NN finding against current code, closes the
"per-tool MCP auth audit" and "wrangler.toml binding cross-environment
audit" scope gaps from the previous round, and records a new scope gap
to systematically audit all remaining optionalAuth endpoints for
similar PII-on-public-read patterns.

bun audit: 0 vulnerabilities. typecheck:all + test:all pass (229
competition-api tests, +3 from this PR).

https://claude.ai/code/session_01HEtzn7C9m7QaRyGbUXwfCW
@github-actions
Copy link
Copy Markdown

Preview Deployment
https://6865933c.glidecomp.pages.dev
Commit: 737b9e2

@pokle pokle marked this pull request as ready for review May 15, 2026 11:59
@pokle pokle merged commit f9fbcf9 into master May 15, 2026
8 checks passed
@pokle pokle deleted the claude/friendly-fermi-UNaes branch May 15, 2026 11:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants