Skip to content

harden: low-severity bundle (rate-limit, TOCTOU, erasure order, outbox)#60

Merged
Musiker15 merged 1 commit into
mainfrom
harden/low-severity-bundle
Jun 20, 2026
Merged

harden: low-severity bundle (rate-limit, TOCTOU, erasure order, outbox)#60
Musiker15 merged 1 commit into
mainfrom
harden/low-severity-bundle

Conversation

@Musiker15

Copy link
Copy Markdown
Member

Bundled low-severity findings from the multi-agent review (PR C — final of the cluster).

Changes

  • Export rate-limitGET /api/submissions/[id]/export now rate-limited (10/min/IP) for parity with withdraw/delete (the UUID is the only credential).
  • Withdraw TOCTOU — read + guarded updateMany (status = current) in one transaction; terminal statuses (incl. an existing withdrawal) return 409 instead of racing.
  • Erasure order — DELETE removes the row first (cascades events + file rows), so the erasure is durable even if MinIO is down, then best-effort purges the stored objects.
  • 0-byte filesContent-Length is set on contentLength !== undefined rather than truthy.
  • Outbox poller — each row delivered under its own try/catch (one bad row no longer aborts the batch); delivered rows marked read in a single updateMany. New partial index notifications(created_at) WHERE read_at IS NULL (hand-written migration) keeps the scan cheap as the table grows.
  • Upload capmaxFileSizeMb capped at a hard server ceiling (MAX_FILE_SIZE_MB = 25) in the spec schema and clamped again in the upload route; bounded in-memory buffering documented.
  • Cleanup — moved a misplaced doc comment onto the function it describes (deliverOne).

Migration

20260620180000_notification_pending_index — raw CREATE INDEX ... WHERE read_at IS NULL (partial indexes aren't expressible in the Prisma schema). Applied by prisma migrate deploy on deploy.

Validation

pnpm typecheck, pnpm lint, pnpm test, pnpm build — all green.

Bundled low-severity findings from the review:

- Rate-limit the self-service export endpoint (10/min/IP) for parity with
  withdraw/delete — the submission UUID is the only credential.
- Withdraw is now race-safe: read + guarded `updateMany (status = current)` in
  one transaction; terminal statuses (incl. an existing withdrawal) return 409.
- Erasure (DELETE) removes the row first (cascade) so it's durable even if
  object storage is down, then best-effort purges the stored objects.
- `GET /api/files/[id]`: set Content-Length on `contentLength !== undefined`
  (0-byte files were silently dropping the header).
- Outbox delivery: deliver each row under its own try/catch (one bad row no
  longer aborts the batch) and mark the delivered rows read in a single
  `updateMany`. Add a partial index on `notifications(created_at) WHERE read_at
  IS NULL` (hand-written migration) to keep the poller scan cheap.
- Cap `maxFileSizeMb` at a hard server ceiling (`MAX_FILE_SIZE_MB = 25`) in the
  spec schema and clamp it again in the upload route; document the bounded
  in-memory buffering.
- Move a misplaced doc comment onto the function it describes (deliverOne).
@Musiker15 Musiker15 merged commit 8d68ff3 into main Jun 20, 2026
3 checks passed
@Musiker15 Musiker15 deleted the harden/low-severity-bundle branch June 20, 2026 17:47
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.

1 participant