From f0f2caab7934e395f4bbaadb5d3baba832aab632 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Tue, 2 Jun 2026 17:25:35 +0200 Subject: [PATCH] fix(medications): bound the RxNorm code length MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The RxCUI field validates against `^\d+$`, which is unbounded — a caller could submit an arbitrarily long digit string. Add a `.max(20)` ceiling as defence-in-depth so the column write stays tight; real RxCUIs are at most a handful of digits. `atcCode` needs no max — its regex already fixes the length. Regenerate the OpenAPI contract for the new `maxLength` on both the create and update bodies. Co-Authored-By: Claude Opus 4.8 (1M context) (cherry picked from commit 8b97d35bc375ef3b2e70f938c0d6729a5fa6bccb) --- docs/api/openapi.yaml | 2 ++ .../__tests__/medication-drug-codes.test.ts | 18 ++++++++++++++++++ src/lib/validations/medication.ts | 5 +++++ 3 files changed, 25 insertions(+) diff --git a/docs/api/openapi.yaml b/docs/api/openapi.yaml index 428463849..ed36dee8b 100644 --- a/docs/api/openapi.yaml +++ b/docs/api/openapi.yaml @@ -2564,6 +2564,7 @@ components: NULL clears it; absent leaves it untouched. anyOf: - type: string + maxLength: 20 pattern: ^\d+$ - type: "null" startsOn: @@ -2782,6 +2783,7 @@ components: NULL clears it; absent leaves it untouched. anyOf: - type: string + maxLength: 20 pattern: ^\d+$ - type: "null" startsOn: diff --git a/src/lib/validations/__tests__/medication-drug-codes.test.ts b/src/lib/validations/__tests__/medication-drug-codes.test.ts index a1c1c0756..79b1b116f 100644 --- a/src/lib/validations/__tests__/medication-drug-codes.test.ts +++ b/src/lib/validations/__tests__/medication-drug-codes.test.ts @@ -71,6 +71,24 @@ describe("medication drug-code validation", () => { .success, ).toBe(false); }); + + it("rejects an over-long RxNorm code (defence-in-depth length bound)", () => { + // The `^\d+$` regex alone is unbounded; the `.max(20)` ceiling + // rejects an absurd digit string while still admitting real RxCUIs. + expect( + createMedicationSchema.safeParse({ + ...baseCreate, + rxNormCode: "1".repeat(21), + }).success, + ).toBe(false); + // 20 digits is the boundary and stays valid. + expect( + createMedicationSchema.safeParse({ + ...baseCreate, + rxNormCode: "1".repeat(20), + }).success, + ).toBe(true); + }); }); describe("updateMedicationSchema", () => { diff --git a/src/lib/validations/medication.ts b/src/lib/validations/medication.ts index 478838bcd..3f560b1f0 100644 --- a/src/lib/validations/medication.ts +++ b/src/lib/validations/medication.ts @@ -51,6 +51,11 @@ const atcCodeField = z const rxNormCodeField = z .string() .regex(RXCUI_REGEX, "Invalid RxNorm code (expected a numeric RxCUI)") + // Defence-in-depth: the `^\d+$` regex is otherwise unbounded. Real + // RxCUIs are at most 7 digits today; 20 is a generous ceiling that + // still bounds the column write tightly. (`atcCode` needs no max — + // its regex already fixes the length at 7.) + .max(20, "RxNorm code is too long") .nullable() .optional() .describe(