v1.1.0:9 — P2 hardening: input-validation 400s, auth rate-limit, XFF anti-spoof, non-root container
CI / proof-of-work (Next.js app) (push) Has been cancelled
CI / start9/0.4 (StartOS package code) (push) Has been cancelled

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.
This commit is contained in:
Keysat
2026-06-13 00:03:47 -05:00
parent 988a3cca9a
commit 3f22ef7600
23 changed files with 365 additions and 41 deletions
+6 -1
View File
@@ -14,6 +14,7 @@ import { v_1_1_0_5 } from './v1.1.0.5'
import { v_1_1_0_6 } from './v1.1.0.6'
import { v_1_1_0_7 } from './v1.1.0.7'
import { v_1_1_0_8 } from './v1.1.0.8'
import { v_1_1_0_9 } from './v1.1.0.9'
/**
* Version graph for the `proof-of-work` package.
@@ -52,9 +53,12 @@ import { v_1_1_0_8 } from './v1.1.0.8'
* v1.1.0:8 — Multi-user authz hardening: whole-instance DB export/import
* admin-only; custom-URL AI providers (Ollama / OpenAI-compatible)
* admin-only + SSRF guard; dead legacy /api/ai/config removed.
* v1.1.0:9 — P2 hardening: malformed-body/invalid-date/bad-pagination ->
* 400 (not 500); POST /api/auth rate-limited; rate-limiter XFF
* anti-spoof (rightmost entry); container drops root via su-exec.
*/
export const versionGraph = VersionGraph.of({
current: v_1_1_0_8,
current: v_1_1_0_9,
other: [
v_1_0_0_1,
v_1_0_0_2,
@@ -70,5 +74,6 @@ export const versionGraph = VersionGraph.of({
v_1_1_0_5,
v_1_1_0_6,
v_1_1_0_7,
v_1_1_0_8,
],
})
+39
View File
@@ -0,0 +1,39 @@
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,
},
})