From 87b761dcf2992ca33d371fbfcbf45aa23510786f Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Mon, 26 Oct 2026 17:02:03 -0700 Subject: [PATCH 1/2] fix(deps): backport zod's leading collection size check so oversized bodies stop validating every element --- apps/sim/lib/api/contracts/chats.test.ts | 14 +++ bun.lock | 5 +- package.json | 3 +- patches/README.md | 20 ++++ patches/zod@4.3.6.patch | 132 +++++++++++++++++++++++ 5 files changed, 171 insertions(+), 3 deletions(-) create mode 100644 patches/zod@4.3.6.patch diff --git a/apps/sim/lib/api/contracts/chats.test.ts b/apps/sim/lib/api/contracts/chats.test.ts index 93368743136..b5b7e9cc992 100644 --- a/apps/sim/lib/api/contracts/chats.test.ts +++ b/apps/sim/lib/api/contracts/chats.test.ts @@ -61,3 +61,17 @@ describe('chat deployment password contract', () => { expect(chatDeploymentPasswordSchema.safeParse('{{PW}}').success).toBe(false) }) }) + +describe('deployed chat body size bound', () => { + /** + * The public chat endpoint validates before authenticating. An oversized `files` + * array must fail on its count alone: walking every element first builds an issue + * per malformed entry, which a small body inflates into an out-of-memory process. + */ + it('rejects an oversized files array without validating its entries', () => { + const result = deployedChatPostBodySchema.safeParse({ files: Array(10_000).fill({}) }) + + expect(result.error?.issues).toHaveLength(1) + expect(result.error?.issues[0]).toMatchObject({ code: 'too_big', path: ['files'] }) + }) +}) diff --git a/bun.lock b/bun.lock index 2748f955925..37837c8a373 100644 --- a/bun.lock +++ b/bun.lock @@ -846,11 +846,12 @@ ], "patchedDependencies": { "echarts@6.1.0": "patches/echarts@6.1.0.patch", - "@better-auth/oauth-provider@1.6.27": "patches/@better-auth%2Foauth-provider@1.6.27.patch", - "drizzle-kit@0.31.10": "patches/drizzle-kit@0.31.10.patch", "next@16.4.1": "patches/next@16.4.1.patch", "oracledb@7.0.1": "patches/oracledb@7.0.1.patch", + "zod@4.3.6": "patches/zod@4.3.6.patch", "postgres@3.4.9": "patches/postgres@3.4.9.patch", + "@better-auth/oauth-provider@1.6.27": "patches/@better-auth%2Foauth-provider@1.6.27.patch", + "drizzle-kit@0.31.10": "patches/drizzle-kit@0.31.10.patch", }, "overrides": { "@ai-sdk/provider-utils": "3.0.28", diff --git a/package.json b/package.json index 78ea6bb3339..a244cc6a5fb 100644 --- a/package.json +++ b/package.json @@ -231,6 +231,7 @@ "postgres@3.4.9": "patches/postgres@3.4.9.patch", "oracledb@7.0.1": "patches/oracledb@7.0.1.patch", "echarts@6.1.0": "patches/echarts@6.1.0.patch", - "next@16.4.1": "patches/next@16.4.1.patch" + "next@16.4.1": "patches/next@16.4.1.patch", + "zod@4.3.6": "patches/zod@4.3.6.patch" } } diff --git a/patches/README.md b/patches/README.md index 96f8f0ef245..c5bc00eff79 100644 --- a/patches/README.md +++ b/patches/README.md @@ -129,3 +129,23 @@ bun run --cwd apps/docs test:negotiation:e2e Remove the patch when upgrading to a release with equivalent Vary preservation, and retain the HTTP regression check. + +# Zod collection size bounds + +`zod@4.3.6` runs an array's `.max()`/`.length()` (and a set's or map's `.max()`/`.size()`) +only after it has parsed every element, so a small body against a bounded schema builds an +issue per malformed element before the bound is reported. An unauthenticated request to the +public chat endpoint can turn under a megabyte of JSON into gigabytes of issues +(GHSA-mrhx-c7xc-m3w5). + +The version-pinned patch backports the leading size gate from +[upstream PR #6652](https://github.com/colinhacks/zod/pull/6652), released in 4.6.6, to the +ESM and CommonJS builds. A container larger than its leading upper bound skips its elements +and returns unchanged; its own size check then reports the single `too_big` issue. Any input +within its bounds parses exactly as before. The bound check is skipped, as upstream, for +`skipChecks` and backward (encode) parses. + +Zod 4.6 is not a drop-in upgrade: it rejects absent `unknown`/`any` object keys, requires +seconds in ISO datetimes, and emits root `$ref` JSON Schema for schemas with an `id`. +`@trigger.dev/core`, which the root override puts on this zod, relies on the first. +Remove this patch when upgrading zod to 4.6.6 or later. diff --git a/patches/zod@4.3.6.patch b/patches/zod@4.3.6.patch new file mode 100644 index 00000000000..11c88531bbf --- /dev/null +++ b/patches/zod@4.3.6.patch @@ -0,0 +1,132 @@ +diff --git a/v4/core/schemas.cjs b/v4/core/schemas.cjs +index 8444c1fa122b0502f56b141ffa2939906b49baf9..9a085e4beacdbe1ef84c4c6a30b471b4aa9f9625 100644 +--- a/v4/core/schemas.cjs ++++ b/v4/core/schemas.cjs +@@ -685,6 +685,37 @@ function handleArrayResult(result, final, index) { + } + final.value[index] = result.value; + } ++// Backport of colinhacks/zod#6652 (zod 4.6.6, GHSA-mrhx-c7xc-m3w5): a container larger than ++// its leading upper size bound skips its elements, so that bound's check (which has a `when` ++// guard and always runs) reports it without parsing and collecting an issue per element first. ++function sizeCaps(defChecks) { ++ const caps = []; ++ for (const ch of defChecks ?? []) { ++ const d = ch._zod.def; ++ // a later check may rewrite the value ++ if (!/_(length|size)$|_equals$/.test(d.check)) ++ break; ++ const max = d.maximum ?? d.length ?? d.size; ++ if (max >= 0) ++ caps.push(max); ++ } ++ return caps; ++} ++function capSize(inst, size) { ++ const caps = sizeCaps(inst._zod.def.checks); ++ if (!caps.length) ++ return; ++ const cap = Math.min(...caps); ++ const parse = inst._zod.parse; ++ inst._zod.parse = (payload, ctx) => { ++ if (size(payload.value) > cap && !ctx?.skipChecks && ctx?.direction !== "backward") ++ return payload; ++ return parse(payload, ctx); ++ }; ++} ++const arraySize = (input) => (Array.isArray(input) ? input.length : -1); ++const mapSize = (input) => (input instanceof Map ? input.size : -1); ++const setSize = (input) => (input instanceof Set ? input.size : -1); + exports.$ZodArray = core.$constructor("$ZodArray", (inst, def) => { + exports.$ZodType.init(inst, def); + inst._zod.parse = (payload, ctx) => { +@@ -718,6 +749,7 @@ exports.$ZodArray = core.$constructor("$ZodArray", (inst, def) => { + } + return payload; //handleArrayResultsAsync(parseResults, final); + }; ++ capSize(inst, arraySize); + }); + function handlePropertyResult(result, final, key, input, isOptionalOut) { + if (result.issues.length) { +@@ -1480,6 +1512,7 @@ exports.$ZodMap = core.$constructor("$ZodMap", (inst, def) => { + return Promise.all(proms).then(() => payload); + return payload; + }; ++ capSize(inst, mapSize); + }); + function handleMapResult(keyResult, valueResult, final, key, input, inst, ctx) { + if (keyResult.issues.length) { +@@ -1540,6 +1573,7 @@ exports.$ZodSet = core.$constructor("$ZodSet", (inst, def) => { + return Promise.all(proms).then(() => payload); + return payload; + }; ++ capSize(inst, setSize); + }); + function handleSetResult(result, final) { + if (result.issues.length) { +diff --git a/v4/core/schemas.js b/v4/core/schemas.js +index 29c61bc5fcf2ca90c2bb0121b7f02e16fdf19f11..f87884eea6e301f1c9c46be45953226e850e4795 100644 +--- a/v4/core/schemas.js ++++ b/v4/core/schemas.js +@@ -654,6 +654,37 @@ function handleArrayResult(result, final, index) { + } + final.value[index] = result.value; + } ++// Backport of colinhacks/zod#6652 (zod 4.6.6, GHSA-mrhx-c7xc-m3w5): a container larger than ++// its leading upper size bound skips its elements, so that bound's check (which has a `when` ++// guard and always runs) reports it without parsing and collecting an issue per element first. ++function sizeCaps(defChecks) { ++ const caps = []; ++ for (const ch of defChecks ?? []) { ++ const d = ch._zod.def; ++ // a later check may rewrite the value ++ if (!/_(length|size)$|_equals$/.test(d.check)) ++ break; ++ const max = d.maximum ?? d.length ?? d.size; ++ if (max >= 0) ++ caps.push(max); ++ } ++ return caps; ++} ++function capSize(inst, size) { ++ const caps = sizeCaps(inst._zod.def.checks); ++ if (!caps.length) ++ return; ++ const cap = Math.min(...caps); ++ const parse = inst._zod.parse; ++ inst._zod.parse = (payload, ctx) => { ++ if (size(payload.value) > cap && !ctx?.skipChecks && ctx?.direction !== "backward") ++ return payload; ++ return parse(payload, ctx); ++ }; ++} ++const arraySize = (input) => (Array.isArray(input) ? input.length : -1); ++const mapSize = (input) => (input instanceof Map ? input.size : -1); ++const setSize = (input) => (input instanceof Set ? input.size : -1); + export const $ZodArray = /*@__PURE__*/ core.$constructor("$ZodArray", (inst, def) => { + $ZodType.init(inst, def); + inst._zod.parse = (payload, ctx) => { +@@ -687,6 +718,7 @@ export const $ZodArray = /*@__PURE__*/ core.$constructor("$ZodArray", (inst, def + } + return payload; //handleArrayResultsAsync(parseResults, final); + }; ++ capSize(inst, arraySize); + }); + function handlePropertyResult(result, final, key, input, isOptionalOut) { + if (result.issues.length) { +@@ -1449,6 +1481,7 @@ export const $ZodMap = /*@__PURE__*/ core.$constructor("$ZodMap", (inst, def) => + return Promise.all(proms).then(() => payload); + return payload; + }; ++ capSize(inst, mapSize); + }); + function handleMapResult(keyResult, valueResult, final, key, input, inst, ctx) { + if (keyResult.issues.length) { +@@ -1509,6 +1542,7 @@ export const $ZodSet = /*@__PURE__*/ core.$constructor("$ZodSet", (inst, def) => + return Promise.all(proms).then(() => payload); + return payload; + }; ++ capSize(inst, setSize); + }); + function handleSetResult(result, final) { + if (result.issues.length) { From f577408387779a69b71f25804abc90adb8d62410 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Mon, 26 Oct 2026 17:20:37 -0700 Subject: [PATCH 2/2] fix(deps): stop later checks from reading an oversized container's unparsed elements --- .../sim/lib/knowledge/transfer/bundle.test.ts | 9 +++ patches/zod@4.3.6.patch | 81 ++++++++++++------- 2 files changed, 62 insertions(+), 28 deletions(-) diff --git a/apps/sim/lib/knowledge/transfer/bundle.test.ts b/apps/sim/lib/knowledge/transfer/bundle.test.ts index 1d1bca7fe4e..36ada0cf1cf 100644 --- a/apps/sim/lib/knowledge/transfer/bundle.test.ts +++ b/apps/sim/lib/knowledge/transfer/bundle.test.ts @@ -71,6 +71,15 @@ describe('knowledgeBundleManifestSchema', () => { ).toBe(false) }) + /** The tag cross-checks read each entry, so they must never see an oversized list unparsed. */ + it('refuses an oversized tag list on its size before the tag cross-checks read it', () => { + const result = knowledgeBundleManifestSchema.safeParse(manifest({ tags: Array(100).fill({}) })) + + expect(result.error?.issues).toEqual([ + expect.objectContaining({ code: 'too_big', path: ['tags'] }), + ]) + }) + it('refuses duplicate tag slots and case-insensitively duplicate tag names', () => { expect( knowledgeBundleManifestSchema.safeParse( diff --git a/patches/zod@4.3.6.patch b/patches/zod@4.3.6.patch index 11c88531bbf..4ce79e1b542 100644 --- a/patches/zod@4.3.6.patch +++ b/patches/zod@4.3.6.patch @@ -1,14 +1,17 @@ +diff --git a/node_modules/zod/.bun-tag-82bf292cf1bc8e88 b/.bun-tag-82bf292cf1bc8e88 +new file mode 100644 +index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/v4/core/schemas.cjs b/v4/core/schemas.cjs -index 8444c1fa122b0502f56b141ffa2939906b49baf9..9a085e4beacdbe1ef84c4c6a30b471b4aa9f9625 100644 +index 8444c1fa122b0502f56b141ffa2939906b49baf9..b1dc056dfe24e7e17927b5b61cbf528fc3830eb8 100644 --- a/v4/core/schemas.cjs +++ b/v4/core/schemas.cjs -@@ -685,6 +685,37 @@ function handleArrayResult(result, final, index) { +@@ -685,6 +685,48 @@ function handleArrayResult(result, final, index) { } final.value[index] = result.value; } +// Backport of colinhacks/zod#6652 (zod 4.6.6, GHSA-mrhx-c7xc-m3w5): a container larger than -+// its leading upper size bound skips its elements, so that bound's check (which has a `when` -+// guard and always runs) reports it without parsing and collecting an issue per element first. ++// its leading upper size bound fails on that bound before any element is parsed, instead of ++// collecting an issue per element first. +function sizeCaps(defChecks) { + const caps = []; + for (const ch of defChecks ?? []) { @@ -18,7 +21,7 @@ index 8444c1fa122b0502f56b141ffa2939906b49baf9..9a085e4beacdbe1ef84c4c6a30b471b4 + break; + const max = d.maximum ?? d.length ?? d.size; + if (max >= 0) -+ caps.push(max); ++ caps.push([max, ch]); + } + return caps; +} @@ -26,12 +29,23 @@ index 8444c1fa122b0502f56b141ffa2939906b49baf9..9a085e4beacdbe1ef84c4c6a30b471b4 + const caps = sizeCaps(inst._zod.def.checks); + if (!caps.length) + return; -+ const cap = Math.min(...caps); -+ const parse = inst._zod.parse; -+ inst._zod.parse = (payload, ctx) => { -+ if (size(payload.value) > cap && !ctx?.skipChecks && ctx?.direction !== "backward") -+ return payload; -+ return parse(payload, ctx); ++ const cap = Math.min(...caps.map((c) => c[0])); ++ const run = inst._zod.run; ++ // gates `run`, not `parse`: 4.3.6 runs `when`-guarded checks even after an abort, so the ++ // failing bound reports once here and no later check reads the unparsed elements ++ inst._zod.run = (payload, ctx) => { ++ const n = size(payload.value); ++ if (n > cap && !ctx?.skipChecks && ctx?.direction !== "backward") { ++ for (const [max, ch] of caps) { ++ if (n > max && ch._zod.def.when(payload)) { ++ const start = payload.issues.length; ++ ch._zod.check(payload); ++ payload.issues[start].continue = false; ++ return payload; ++ } ++ } ++ } ++ return run(payload, ctx); + }; +} +const arraySize = (input) => (Array.isArray(input) ? input.length : -1); @@ -40,7 +54,7 @@ index 8444c1fa122b0502f56b141ffa2939906b49baf9..9a085e4beacdbe1ef84c4c6a30b471b4 exports.$ZodArray = core.$constructor("$ZodArray", (inst, def) => { exports.$ZodType.init(inst, def); inst._zod.parse = (payload, ctx) => { -@@ -718,6 +749,7 @@ exports.$ZodArray = core.$constructor("$ZodArray", (inst, def) => { +@@ -718,6 +760,7 @@ exports.$ZodArray = core.$constructor("$ZodArray", (inst, def) => { } return payload; //handleArrayResultsAsync(parseResults, final); }; @@ -48,7 +62,7 @@ index 8444c1fa122b0502f56b141ffa2939906b49baf9..9a085e4beacdbe1ef84c4c6a30b471b4 }); function handlePropertyResult(result, final, key, input, isOptionalOut) { if (result.issues.length) { -@@ -1480,6 +1512,7 @@ exports.$ZodMap = core.$constructor("$ZodMap", (inst, def) => { +@@ -1480,6 +1523,7 @@ exports.$ZodMap = core.$constructor("$ZodMap", (inst, def) => { return Promise.all(proms).then(() => payload); return payload; }; @@ -56,7 +70,7 @@ index 8444c1fa122b0502f56b141ffa2939906b49baf9..9a085e4beacdbe1ef84c4c6a30b471b4 }); function handleMapResult(keyResult, valueResult, final, key, input, inst, ctx) { if (keyResult.issues.length) { -@@ -1540,6 +1573,7 @@ exports.$ZodSet = core.$constructor("$ZodSet", (inst, def) => { +@@ -1540,6 +1584,7 @@ exports.$ZodSet = core.$constructor("$ZodSet", (inst, def) => { return Promise.all(proms).then(() => payload); return payload; }; @@ -65,16 +79,16 @@ index 8444c1fa122b0502f56b141ffa2939906b49baf9..9a085e4beacdbe1ef84c4c6a30b471b4 function handleSetResult(result, final) { if (result.issues.length) { diff --git a/v4/core/schemas.js b/v4/core/schemas.js -index 29c61bc5fcf2ca90c2bb0121b7f02e16fdf19f11..f87884eea6e301f1c9c46be45953226e850e4795 100644 +index 29c61bc5fcf2ca90c2bb0121b7f02e16fdf19f11..1123f503df65ff9993a0e04a4c78dfb3e216b961 100644 --- a/v4/core/schemas.js +++ b/v4/core/schemas.js -@@ -654,6 +654,37 @@ function handleArrayResult(result, final, index) { +@@ -654,6 +654,48 @@ function handleArrayResult(result, final, index) { } final.value[index] = result.value; } +// Backport of colinhacks/zod#6652 (zod 4.6.6, GHSA-mrhx-c7xc-m3w5): a container larger than -+// its leading upper size bound skips its elements, so that bound's check (which has a `when` -+// guard and always runs) reports it without parsing and collecting an issue per element first. ++// its leading upper size bound fails on that bound before any element is parsed, instead of ++// collecting an issue per element first. +function sizeCaps(defChecks) { + const caps = []; + for (const ch of defChecks ?? []) { @@ -84,7 +98,7 @@ index 29c61bc5fcf2ca90c2bb0121b7f02e16fdf19f11..f87884eea6e301f1c9c46be45953226e + break; + const max = d.maximum ?? d.length ?? d.size; + if (max >= 0) -+ caps.push(max); ++ caps.push([max, ch]); + } + return caps; +} @@ -92,12 +106,23 @@ index 29c61bc5fcf2ca90c2bb0121b7f02e16fdf19f11..f87884eea6e301f1c9c46be45953226e + const caps = sizeCaps(inst._zod.def.checks); + if (!caps.length) + return; -+ const cap = Math.min(...caps); -+ const parse = inst._zod.parse; -+ inst._zod.parse = (payload, ctx) => { -+ if (size(payload.value) > cap && !ctx?.skipChecks && ctx?.direction !== "backward") -+ return payload; -+ return parse(payload, ctx); ++ const cap = Math.min(...caps.map((c) => c[0])); ++ const run = inst._zod.run; ++ // gates `run`, not `parse`: 4.3.6 runs `when`-guarded checks even after an abort, so the ++ // failing bound reports once here and no later check reads the unparsed elements ++ inst._zod.run = (payload, ctx) => { ++ const n = size(payload.value); ++ if (n > cap && !ctx?.skipChecks && ctx?.direction !== "backward") { ++ for (const [max, ch] of caps) { ++ if (n > max && ch._zod.def.when(payload)) { ++ const start = payload.issues.length; ++ ch._zod.check(payload); ++ payload.issues[start].continue = false; ++ return payload; ++ } ++ } ++ } ++ return run(payload, ctx); + }; +} +const arraySize = (input) => (Array.isArray(input) ? input.length : -1); @@ -106,7 +131,7 @@ index 29c61bc5fcf2ca90c2bb0121b7f02e16fdf19f11..f87884eea6e301f1c9c46be45953226e export const $ZodArray = /*@__PURE__*/ core.$constructor("$ZodArray", (inst, def) => { $ZodType.init(inst, def); inst._zod.parse = (payload, ctx) => { -@@ -687,6 +718,7 @@ export const $ZodArray = /*@__PURE__*/ core.$constructor("$ZodArray", (inst, def +@@ -687,6 +729,7 @@ export const $ZodArray = /*@__PURE__*/ core.$constructor("$ZodArray", (inst, def } return payload; //handleArrayResultsAsync(parseResults, final); }; @@ -114,7 +139,7 @@ index 29c61bc5fcf2ca90c2bb0121b7f02e16fdf19f11..f87884eea6e301f1c9c46be45953226e }); function handlePropertyResult(result, final, key, input, isOptionalOut) { if (result.issues.length) { -@@ -1449,6 +1481,7 @@ export const $ZodMap = /*@__PURE__*/ core.$constructor("$ZodMap", (inst, def) => +@@ -1449,6 +1492,7 @@ export const $ZodMap = /*@__PURE__*/ core.$constructor("$ZodMap", (inst, def) => return Promise.all(proms).then(() => payload); return payload; }; @@ -122,7 +147,7 @@ index 29c61bc5fcf2ca90c2bb0121b7f02e16fdf19f11..f87884eea6e301f1c9c46be45953226e }); function handleMapResult(keyResult, valueResult, final, key, input, inst, ctx) { if (keyResult.issues.length) { -@@ -1509,6 +1542,7 @@ export const $ZodSet = /*@__PURE__*/ core.$constructor("$ZodSet", (inst, def) => +@@ -1509,6 +1553,7 @@ export const $ZodSet = /*@__PURE__*/ core.$constructor("$ZodSet", (inst, def) => return Promise.all(proms).then(() => payload); return payload; };