From d101aa899de474ca0a0ef1f78edc7488398887fe Mon Sep 17 00:00:00 2001 From: Lucas Berger Date: Wed, 10 Jun 2026 16:51:06 -0400 Subject: [PATCH] fix(06): CR-01 validate recurrenceUntil as YYYY-MM-DD to close RRULE injection --- apps/api/src/broker/outboxWorker.ts | 7 +++++-- apps/api/src/routes/events.ts | 7 +++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/apps/api/src/broker/outboxWorker.ts b/apps/api/src/broker/outboxWorker.ts index 1d73a25..5e6a9da 100644 --- a/apps/api/src/broker/outboxWorker.ts +++ b/apps/api/src/broker/outboxWorker.ts @@ -79,8 +79,11 @@ const outboxPayloadSchema = z calendarUrl: z.string().url().max(1024).optional(), _preservedRrule: z.string().max(1024).optional(), // D-06: recurrence bounding (RRULE UNTIL / COUNT) - // T-06-02: max(10) bounds 'YYYY-MM-DD'; int().min(1) prevents zero/negative counts - recurrenceUntil: z.string().max(10).optional(), // 'YYYY-MM-DD' → RRULE UNTIL + // CR-01: defense-in-depth — validate the exact 'YYYY-MM-DD' shape here too (the route + // schema validates on ingress, but the outbox payload is re-parsed from stored JSON). + // Guarantees .replace(/-/g,'') in assembleRruleString emits digits-only, closing the + // RRULE-part injection vector. int().min(1) prevents zero/negative counts. + recurrenceUntil: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional(), // 'YYYY-MM-DD' → RRULE UNTIL recurrenceCount: z.number().int().min(1).optional(), // integer ≥ 1 → RRULE COUNT }) .passthrough() diff --git a/apps/api/src/routes/events.ts b/apps/api/src/routes/events.ts index b7a2603..c57558e 100644 --- a/apps/api/src/routes/events.ts +++ b/apps/api/src/routes/events.ts @@ -107,8 +107,11 @@ const eventFieldsSchema = z.object({ recurrence: z.enum(['none', 'daily', 'weekly', 'monthly', 'yearly']).optional(), calendarUrl: z.string().url().max(1024).optional(), // D-06: recurrence bounding (RRULE UNTIL / COUNT) - // T-06-02: max(10) bounds 'YYYY-MM-DD'; int().min(1) prevents zero/negative counts - recurrenceUntil: z.string().max(10).optional(), // 'YYYY-MM-DD' → RRULE UNTIL + // CR-01: validate the exact 'YYYY-MM-DD' shape (reusing eventsQuerySchema's regex) so a + // ≤10-char non-date string cannot survive .replace(/-/g,'') and inject extra ';'-delimited + // RRULE parts when spliced into the UNTIL template (outboxWorker.assembleRruleString). + // int().min(1) prevents zero/negative counts. + recurrenceUntil: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional(), // 'YYYY-MM-DD' → RRULE UNTIL recurrenceCount: z.number().int().min(1).optional(), // integer ≥ 1 → RRULE COUNT })