From 24379721d1c4a76494c62453eb9a0c2f8f7ee974 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Sun, 17 May 2026 19:02:44 +0200 Subject: [PATCH] fix(auth): validate User.timezone against IANA zone list on profile update MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The shared profile-update schema accepted any 1-64 character string for the timezone field and persisted it directly into User.timezone. Downstream callers — most notably the v1.4.38 dashboard summary route — pass the raw stored value into postgres via `to_char(measured_at AT TIME ZONE ${userTz})`, which hard-fails with `invalid_parameter_value` for any unknown zone. Before v1.4.38 the field was consumed only through the defensive isValidTimezone wrapper in @/lib/tz/format and silently fell back to Europe/Berlin, so the gap was latent; the new SQL path made it a self-DoS regression on any account that ever wrote a junk zone. Chains the existing isValidTimezone helper into the Zod schema as a refine so the API now returns 422 "Invalid IANA timezone" at the write boundary instead of writing junk and failing on read. --- src/lib/auth/profile-update.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/lib/auth/profile-update.ts b/src/lib/auth/profile-update.ts index 78fe5e9fb..b8d74da65 100644 --- a/src/lib/auth/profile-update.ts +++ b/src/lib/auth/profile-update.ts @@ -6,12 +6,18 @@ import { prisma } from "@/lib/db"; import { auditLog } from "@/lib/auth/audit"; import { profileSchema } from "@/lib/validations/auth"; +import { isValidTimezone } from "@/lib/tz/format"; import { z } from "zod/v4"; const extendedProfileSchema = profileSchema.extend({ displayName: z.string().min(1).max(80).nullable().optional(), locale: z.enum(["de", "en"]).nullable().optional(), - timezone: z.string().min(1).max(64).optional(), + timezone: z + .string() + .min(1) + .max(64) + .refine(isValidTimezone, "Invalid IANA timezone") + .optional(), }); export type ApplyProfileInput = z.infer;