Zod Security — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited Zod Security (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Zod guards data correctness at runtime boundaries. It cannot prevent logic errors in business code, SQL injection in query strings, or XSS in template rendering. What it does prevent — when used correctly — is:
When used incorrectly, Zod creates a false sense of security — code appears to validate but allows unsafe data through.
z.strictObject() at Public API Boundariesz.object() silently strips unknown keys. z.strictObject() actively rejects them. The distinction is security-critical at external-facing endpoints:
// VULNERABLE: Strips extra keys but never rejects them
// POST /user { name: "Alice", role: "admin" } — "role" is silently stripped
// Developer assumes "role" can't be injected; business logic still may be unsafe
const CreateUserSchema = z.object({ name: z.string() });
// SECURE: Rejects requests containing unexpected keys
const CreateUserSchema = z.strictObject({ name: z.string() });
// POST /user { name: "Alice", role: "admin" } → 400 Bad RequestUse `z.strictObject()` for: All POST/PUT/PATCH body schemas at API entry points. Use `z.object()` for: Internal data transformation, reshaping data between layers.
z.custom() Without a Validatorz.custom() with no argument (or a validator that always returns true) is a validation bypass — it accepts any value:
// DANGEROUS: Equivalent to z.unknown() — validates nothing
const DangerousSchema = z.custom();
const AlsoDangerous = z.custom(() => true);
// SAFE: Always provide a type guard
const SafeSchema = z.custom<Date>((val) => val instanceof Date);
const SafeBuffer = z.custom<Buffer>((val) => Buffer.isBuffer(val));Flag any z.custom() call in a code review that lacks a meaningful predicate function.
In Zod 4, `.default()` values are always applied, even when a field is wrapped in .optional() or .partial(). Reusing a create-schema (which has defaults) for a PATCH endpoint causes silent data corruption:
const UserSchema = z.object({
name: z.string(),
bio: z.string().default(""), // Default: empty string
role: z.enum(["user", "admin"]).default("user"), // Default: "user"
});
// DANGEROUS: PATCH with partial user (only updating name)
// Input: { name: "Alice" }
// After parse: { name: "Alice", bio: "", role: "user" }
// → Overwrites existing bio and role with defaults!
const PatchUserSchema = UserSchema.partial(); // Still applies defaults ❌
// SAFE: Create a dedicated patch schema without defaults
const PatchUserSchema = z.object({
name: z.string().optional(),
bio: z.string().optional(),
role: z.enum(["user", "admin"]).optional(),
});
// Input: { name: "Alice" } → Output: { name: "Alice" }
// Only provided fields are present; omitted fields stay undefinedAlways create dedicated schemas for partial update endpoints. Never reuse creation schemas with .partial().
z.coerce.* accepts unknown input and applies the JavaScript constructor. Unconstrained coercion can produce surprising values:
// Surprising coercions:
z.coerce.number().parse("0xff") // → 255 (hex string parsed as number!)
z.coerce.number().parse("") // → 0
z.coerce.boolean().parse("false") // → true (any non-empty string is truthy)
z.coerce.date().parse("garbage") // → Invalid Date (no error!)Constrain input before coercing:
// SAFE: Constrain to expected types, then coerce
const AgeSchema = z.string().regex(/^\d+$/, "Must be a numeric string")
.pipe(z.coerce.number().int().min(0).max(150));
// SAFE: Reject non-finite numbers after coercion
const PriceSchema = z.string()
.pipe(z.coerce.number().finite().positive());Returning the raw ZodError or its .issues array in HTTP responses exposes internal schema structure:
// VULNERABLE: Reveals field names, path structure, internal model
res.status(400).json({ error: validationError });
// SECURE: Sanitize for client consumption
res.status(400).json({
message: "Validation failed",
errors: z.flattenError(validationError).fieldErrors,
});reportInput: true PII RiskSetting reportInput: true in error configuration causes Zod to include the raw input value in error issues. In production, this logs passwords, tokens, and personal data:
// NEVER in production
z.config({ reportInput: true });Only enable reportInput in local development when debugging. Treat it like a debug flag — commit it off, never ship it on.
.passthrough() and z.looseObject() at BoundariesBoth explicitly allow unknown keys through. Never use at public API entry points:
// VULNERABLE: Allows any extra fields through
const Schema = z.object({ name: z.string() }).passthrough();
const Schema = z.looseObject({ name: z.string() });
// Only use .passthrough() when explicitly proxying data you don't own
// (e.g., forwarding third-party webhook payloads)When reviewing TypeScript code that uses Zod, verify:
| Check | Good | Red Flag |
|---|---|---|
| Object schemas at API boundaries | z.strictObject() | z.object() |
| Custom validators | z.custom(val => val instanceof X) | z.custom() or z.custom(() => true) |
| PATCH/partial update schemas | Dedicated schema, no defaults | CreationSchema.partial() |
| Coercion pattern | Constrained input → pipe → coerce | Bare z.coerce.* on untrusted input |
| Error responses | z.flattenError() / z.treeifyError() | Raw ZodError to client |
| reportInput flag | Not present in production code | reportInput: true committed |
| Unknown key handling | .passthrough() only for explicit proxy use | .passthrough() at API endpoints |
| Schema hoisting | Module-level constants | Schemas defined inside handlers |
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.