fix(core): honour jitless config in allowsEval probe#5864
fix(core): honour jitless config in allowsEval probe#5864dokson wants to merge 1 commit intocolinhacks:mainfrom
jitless config in allowsEval probe#5864Conversation
Closes colinhacks#4461, closes colinhacks#5414. `allowsEval` (packages/zod/src/v4/core/util.ts) feature-probes for `new Function()` support to pick Zod's JIT fast-path. The probe runs inside try/catch so it never throws at runtime — but strict-CSP browsers (script-src without 'unsafe-eval') still log the caught attempt as a `securitypolicyviolation`, which Chrome DevTools surfaces as an Issue ("Content Security Policy of your site blocks the use of 'eval'"). Many users have reported this as noise in production error monitors; the documented workaround `z.config({ jitless: true })` currently doesn't help because the probe is memoised via `cached()` and runs lazily on first `.value` access regardless of config. This change makes `allowsEval` read `globalConfig.jitless` before attempting the probe. When `jitless` is true, return `false` early and skip `new Function("")` entirely — no CSP violation is generated. The existing `schemas.ts:2025` already gates the fast-path on `!jitless`, so users who opt in are already on the slow path; the probe was just dead weight on their page loads. - No default-behaviour change: when `jitless` is undefined (the default), `allowsEval` still probes as before. - Strictly more restrictive when `jitless=true`: no eval attempt, no cached path to it — safer on strict CSPs, no library-side regression. - Users must call `z.config({ jitless: true })` at application entry, before any schema is parsed, because `allowsEval.value` is memoised via `cached()` on first access. Tests: packages/zod/src/v4/classic/tests/jitless-allows-eval.test.ts stubs the `Function` global and asserts the probe is never invoked under `z.config({ jitless: true })`. Lives in its own file so vitest's per-file ESM isolation gives a fresh cached allowsEval.
|
Heads-up: the one failing CI job ( This PR only touches Happy to add |
Summary
Makes
z.config({ jitless: true })actually silence the Chrome DevTools "Content Security Policy of your site blocks the use of 'eval' in JavaScript" issue on strict-CSP pages, by short-circuiting thenew Function("")feature probe insideallowsEval.Closes #4461.
Closes #5414.
Problem
util.allowsEvalfeature-probes fornew Function()support so the JIT fast-path inschemas.tscan be enabled opportunistically. The probe is wrapped intry/catch, so it never throws at runtime — but browsers still log the caught attempt as asecuritypolicyviolation, which Chrome surfaces as a Developer Tools Issue:Users on strict CSPs (
script-srcwithout'unsafe-eval') have flagged this in production error monitors (Datadog, Sentry) — see #5414 where @zbauman3 reported "literally thousands of errors" from a single Zod v4 upgrade. The maintainer's documented workaround was:But this doesn't actually help today, because
allowsEvalis memoised viacached()and runs lazily on first.valueaccess, regardless of whether the user setjitlessfirst. Users reported back that settingjitlessdid nothing for the CSP violation, and the thread has stalled.Fix
allowsEvalnow consultsglobalConfig.jitlessbefore attempting the probe. Whenjitless === true, it returnsfalsewithout invokingnew Functionat all — no CSP violation is ever generated.The change is minimal, additive, and strictly more restrictive when opted into. The existing
jit = !core.globalConfig.jitlessgate atschemas.ts:2025already routesjitless=trueusers onto the slow path; the probe was just dead weight on their page loads.API / behaviour
jitlessisundefined(its default),allowsEvalprobes exactly as before — no behaviour shift for the 99% of users who never touch config.jitless=true, Zod never callsnew Function(""). Users on strict CSPs now have a clean devtools console.z.config({ jitless: true })must be called at application entry, before any schema is parsed, becauseallowsEval.valueis memoised viacached()on first access. Same contract thejitlessoption already implies — this PR doesn't change it.jitless(one extraif (undefined)branch on first probe).Tests
Added
packages/zod/src/v4/classic/tests/jitless-allows-eval.test.ts. It:z.config({ jitless: true })at the top of the test.globalThis.Functionwith a stub that throws if invoked.util.allowsEval.value === false.The test lives in its own file because vitest isolates ESM graphs per file, so the cached
allowsEval.valueis guaranteed fresh and is never accessed before the config mutation. Afinallyblock restoresglobalThis.Functionand clears the config flag so other test files in the same worker see the default.Regression check
Ran the full suite locally (
pnpm test). All existing tests pass exceptsrc/v4/classic/tests/datetime.test.ts > redos checker, which times out at 5000ms → 13800ms on my Windows/pnpm combo when run under parallel-file concurrency. Isolated run (pnpm test packages/zod/src/v4/classic/tests/datetime.test.ts) passes cleanly in 11.5s / 28 tests. The failure is environmental (ReDoS-check regex timing on a slower machine) and completely unrelated toutil.ts— same test passes onmainwithout this branch. CI on GitHub Actions (Linux) should be clean.Scope / risk
packages/zod/src/v4/core/util.ts(+12 lines, new runtime import + early return).packages/zod/src/v4/classic/tests/jitless-allows-eval.test.ts(+46 lines).$ZodConfig.jitlessalready exists onmainand is documented atcore.ts:129.Happy to iterate on the test style, the comment wording, or split the changelog/docs note into a separate PR if preferred.