Skip to content

The public signup endpoint returns the whole database row, internal columns included #452

Closed

Description

@richshaw

Needs your OK: this removes fields from a public API response (GET /api/public/signups/[slug]). Nothing in the app calls it, but anyone scripting against it would notice. Swap pipeline:proposed for pipeline:ready to have it built.

What happens

getPublicSignup in src/services/signups.ts selects every column of signups and every column of slots, and src/app/api/public/signups/[slug]/route.ts hands the result straight to respond(). So the unauthenticated endpoint returns, besides what a participant needs:

  • signup: workspaceId, organizerId, claimTokenHash (never written, always null), tags, all of settings (including lockoutHoursBeforeSlot, reminderFromFieldRef, confirmationMessage), deletedAt, createdAt, updatedAt
  • each slot: workspaceId, signupId, priceCents, createdAt, updatedAt

None of this gives access to anything; every organizer route checks ids against the policy layer. The problem is the default: any server-only column added to signups or slots later goes public without anyone deciding it should. #447 is the same problem for commitments.

The /s/[slug] page is fine: it passes only picked fields to its client components (toSignupViewSlots, toSignupViewFields).

Fix, in outline

  • Add a toPublicSignup() mapping with an explicit list of fields: slug, title, description, status, opensAt, closesAt; from settings only requireEmail, sendReminders, groupByFieldRefs; fields as ref, label, fieldType, config; slots in the same shape toSignupViewSlots gives the page; and committedBySlot.
  • Apply it in the route before respond(). If it's simple, have the page reuse the same slot shape so the two can't drift.

Watch for

  • The page still needs id and workspaceId server-side for view tracking, so don't narrow what getPublicSignup returns to the page; narrow what the route sends.
  • src/app/query-budget.db.test.ts caps statements for the public page. This is a mapping, so it should add none.

Choices this makes

  • Field list: the default is the list above, which is what the public page itself shows. tags and the other organizer settings are left out.

Done when

  • The route responds through the allow-list.
  • A test lists the response's keys (top level, slot, field) and fails if one is added.
  • pnpm lint && pnpm typecheck && pnpm knip && pnpm test && pnpm test:db pass.

Found by the pipeline's security scout in the participant flow (src/app/api/public, src/services/signups.ts), at a9bcff7. Checked by an independent DB test that called the route handler on a published signup and listed the keys returned. https://claude.ai/code/session_01H1WEpHa6h8WwVYLTAKoqhg

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions