3f22ef7600
P2 batch from the 2026-06-13 full-eval (EVALUATION.md / ROADMAP.md), reviewed by the reviewer agent. App-code + packaging only; no schema or data change, existing /data untouched. Input validation: malformed JSON bodies, invalid date, and out-of-range or non-numeric pagination on /api/workouts now return 400 instead of 500. New lib/http.ts readJsonBody maps a bad body to a ZodError across the 11 CRUD routes whose catch maps ZodError to 400; me/import and admin/signups guard request.json() in an explicit try/catch. Rate limiting: POST /api/auth now shares the UI login server action's per-IP 10-per-15min cap and returns 429 + Retry-After. clientIpFromHeaders reads the rightmost (trusted-proxy-appended) X-Forwarded-For entry instead of the spoofable leftmost. Container: drops root. The entrypoint prepares /data as root, chowns it to nextjs, then exec su-exec nextjs:nodejs node server.js (su-exec added to the runner image). The container drop needs live sideload verification.
40 lines
2.1 KiB
TypeScript
40 lines
2.1 KiB
TypeScript
import { IMPOSSIBLE, VersionInfo } from '@start9labs/start-sdk'
|
|
|
|
/**
|
|
* v1.1.0:9 — P2 hardening batch (2026-06-13, follows the :8 security batch).
|
|
*
|
|
* Input-validation, rate-limiting and container hardening from the full-eval
|
|
* P2 queue (see EVALUATION.md / ROADMAP.md):
|
|
*
|
|
* - Malformed request bodies now return 400 instead of 500. New
|
|
* lib/http.ts `readJsonBody` maps a bad JSON body to a ZodError across the
|
|
* 11 body-parsing CRUD routes (which already map ZodError -> 400);
|
|
* /api/me/import and /api/admin/signups guard it explicitly (safeParse
|
|
* style). Invalid `date` and out-of-range/non-numeric pagination on
|
|
* /api/workouts are likewise 400, not a Prisma 500.
|
|
* - POST /api/auth (the raw login API) is now rate-limited with the same
|
|
* per-IP 10/15min cap as the UI login server action, sharing the
|
|
* `login:${ip}` bucket — previously an uncapped credential-stuffing
|
|
* surface. Returns 429 + Retry-After.
|
|
* - The rate limiter's client-IP detection now reads the rightmost
|
|
* (trusted-proxy-appended) X-Forwarded-For entry instead of the spoofable
|
|
* leftmost one, so a forged XFF can't rotate the limiter key.
|
|
* - The container drops root: the entrypoint still prepares /data as root,
|
|
* then chowns it to `nextjs` and `exec su-exec`s the Node server as the
|
|
* unprivileged uid 1001 — shrinking the blast radius of any app RCE.
|
|
*
|
|
* App-code + packaging only — no schema, no API contract change for existing
|
|
* data, no data migration. Existing /data survives untouched.
|
|
*/
|
|
export const v_1_1_0_9 = VersionInfo.of({
|
|
version: '1.1.0:9',
|
|
releaseNotes: {
|
|
en_US:
|
|
'Hardening. Bad or malformed requests now return clean validation errors instead of server errors. The login API is rate-limited (matching the web form) and the limiter can no longer be fooled by spoofed forwarding headers. The app container now runs as an unprivileged user instead of root, reducing the impact of any future vulnerability. No schema or data changes — your existing data is untouched.',
|
|
},
|
|
migrations: {
|
|
up: async () => {},
|
|
down: IMPOSSIBLE,
|
|
},
|
|
})
|