From c2f436720f25f10f4c2c516f9c216e76a468047e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Sun, 17 May 2026 15:22:29 +0200 Subject: [PATCH] fix(measurements): reject impossible dayKey values (2026-02-30 etc) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The dayKey validator regex matched any YYYY-MM-DD string — including impossible dates like 2026-02-30 or 2026-13-01. `new Date()` silently overflows those values into the following month, so the drill-down returned rows from a different calendar day than the user asked for. No 422 was emitted; the response was syntactically well-formed and pointed at the wrong day. The same helper feeds the admin drain route, so a malformed CLI invocation had the same blast radius. Add a refine that round-trips the parsed Date back to ISO and rejects when the slice no longer matches the input. --- .../__tests__/group-by-day.test.ts | 19 +++++++++++++++++++ src/lib/validations/measurement.ts | 16 ++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/src/app/api/measurements/__tests__/group-by-day.test.ts b/src/app/api/measurements/__tests__/group-by-day.test.ts index 50f43de51..f4c91885b 100644 --- a/src/app/api/measurements/__tests__/group-by-day.test.ts +++ b/src/app/api/measurements/__tests__/group-by-day.test.ts @@ -284,4 +284,23 @@ describe("GET /api/measurements — schema rejections (W10 reconcile)", () => { expect(prisma.measurement.findMany).not.toHaveBeenCalled(); }); + // v1.4.37 W10 H2 — `2026-02-30` is a YYYY-MM-DD string but not a + // real calendar date. `new Date()` silently overflows it to March + // 2, so the drill-down would point at the wrong day. The validator + // refine must reject it at parse time. + it("rejects impossible calendar dates on dayKey (2026-02-30)", async () => { + const res = await GET( + getRequest("type=ACTIVITY_STEPS&dayKey=2026-02-30"), + ); + expect(res.status).toBe(422); + expect(prisma.measurement.findMany).not.toHaveBeenCalled(); + }); + + it("rejects impossible month on dayKey (2026-13-01)", async () => { + const res = await GET( + getRequest("type=ACTIVITY_STEPS&dayKey=2026-13-01"), + ); + expect(res.status).toBe(422); + expect(prisma.measurement.findMany).not.toHaveBeenCalled(); + }); }); diff --git a/src/lib/validations/measurement.ts b/src/lib/validations/measurement.ts index 081541700..ec6ee64ee 100644 --- a/src/lib/validations/measurement.ts +++ b/src/lib/validations/measurement.ts @@ -299,6 +299,22 @@ export const listMeasurementsSchema = z dayKey: z .string() .regex(/^\d{4}-\d{2}-\d{2}$/, "dayKey must be YYYY-MM-DD") + // v1.4.37 W10 — a `YYYY-MM-DD` string can satisfy the regex while + // still being an impossible calendar date (`2026-02-30`, + // `2026-13-01`). `new Date("2026-02-30T00:00:00Z")` silently + // overflows to March 2, so the drill-down would return rows from + // a different day than the user asked for. The same helper feeds + // the admin drain route, so a malformed CLI invocation has the + // same blast radius. Reject the impossible shapes at the + // validator instead. + .refine( + (s) => { + const parsed = new Date(`${s}T00:00:00Z`); + if (Number.isNaN(parsed.getTime())) return false; + return s === parsed.toISOString().slice(0, 10); + }, + "dayKey must be a real calendar date (YYYY-MM-DD)", + ) .optional(), }) .refine(