From 2c3eeee39bad1e6ce82ebf5dcd7601a75e382bf0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Thu, 21 May 2026 12:43:28 +0200 Subject: [PATCH] fix(consent): enforce artefact 64 KB cap via Buffer.byteLength (not char-count) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `z.string().max()` counts JavaScript string units (UTF-16 code units), not bytes. The audit-table guarantee for SB-10 consent artefacts is byte-bounded — a UTF-8 artefact full of multi-byte code points (CJK, emoji) would otherwise slip past the 64 KB row budget. Replace the `.max()` with a `.refine()` that uses `Buffer.byteLength(value, "utf8")`. Closes security M1 from the v1.4.40 QA round. --- src/lib/validations/consent.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/lib/validations/consent.ts b/src/lib/validations/consent.ts index bc03788cc..13427af39 100644 --- a/src/lib/validations/consent.ts +++ b/src/lib/validations/consent.ts @@ -29,6 +29,13 @@ export type ConsentKind = z.infer; * under 32 KB; a signed JWT is a few hundred bytes. The cap exists so * a misbehaving client (or a malicious one) can't fill the audit table * with multi-megabyte rows. + * + * v1.4.40 security M1 — the cap is enforced via `Buffer.byteLength(..., + * "utf8")` because `z.string().max()` counts JavaScript string units + * (UTF-16 code units), not bytes. A UTF-8 artefact full of multi-byte + * code points (CJK, emoji) would otherwise be allowed past the 64 KB + * row budget — the database stores bytes, not code units, and the + * audit-table guarantee is byte-bounded. */ const ARTEFACT_MAX_BYTES = 64 * 1024; @@ -37,7 +44,10 @@ export const consentPostBody = z.object({ artefact: z .string() .min(1, "artefact must not be empty") - .max(ARTEFACT_MAX_BYTES, "artefact exceeds 64 KB cap"), + .refine( + (value) => Buffer.byteLength(value, "utf8") <= ARTEFACT_MAX_BYTES, + { message: "artefact exceeds 64 KB cap (UTF-8 byte length)" }, + ), signedAt: z.iso .datetime({ offset: true }) .transform((s) => new Date(s)),