Client-side validation is UX; server-side validation is security. When the two diverge, the gap is exploitable: a client that allows $0.01-$9,999.99 but a server with no upper bound means any user who bypasses the form (curl, Burp, modified JS) can submit arbitrary amounts. CWE-602 (Client-Side Enforcement of Server-Side Security) and OWASP A03 both flag this pattern. Parity isn't just a correctness concern — inconsistent constraints produce confusing UX when server validation rejects what the client accepted.
Low because mismatched constraints require an attacker to bypass client validation, which is trivial but limits casual exploitation to motivated users.
Extract shared validation into a single Zod schema in src/lib/schemas.ts that both the React form resolver and the API route import. This mechanically prevents drift.
// src/lib/schemas.ts
export const transferSchema = z.object({
amountCents: z.number().int().min(1, 'Minimum $0.01').max(99999, 'Maximum $999.99'),
routingNumber: z.string().refine(isValidABARoutingNumber, 'Invalid routing number'),
});
// src/components/TransferForm.tsx
const form = useForm({ resolver: zodResolver(transferSchema) });
// src/app/api/transfers/route.ts
const data = transferSchema.parse(await req.json());
ID: finserv-form-validation.calculation-accuracy.client-server-parity
Severity: low
What to look for: List all financial validation constraints (min, max, format, required). For each constraint, check whether it exists in both client-side (form component) and server-side (API route). Count the total constraint pairs and classify each as matched (same values both sides), mismatched (different values), or one-sided (exists on only one layer). Quote any mismatches found. Report: "X of Y validation constraints are matched on both layers."
Pass criteria: At least 100% of financial validation constraints match between client and server (same min, max, format rules). No more than 0 constraints are one-sided or mismatched. A shared schema (imported by both layers) automatically satisfies this.
Fail criteria: Any financial constraint differs between client and server, or exists on only one layer.
Should not pass when: Client-side validation is stricter than server-side — the server must be at least as strict as the client since client validation is bypassable.
Skip (N/A) when: The project has no financial forms (0 financial validation constraints found).
Detail on fail: Example: "Client-side validation allows amounts 0.01-99999.99. Server-side API accepts any amount without max limit. Server constraint missing." or "Client accepts IBAN, server rejects without error message"
Cross-reference: The negative-amounts-blocked check specifically verifies dual-layer negative rejection; this check covers all constraint types holistically.
Remediation: Extract common validation into a shared schema in src/lib/schemas.ts:
Shared validation schema in src/lib/schemas.ts:
export const transferSchema = z.object({
amountCents: z.number()
.int('Amount must be in cents')
.min(1, 'Minimum $0.01')
.max(999999, 'Maximum $9,999.99'),
accountNumber: z.string().refine(validateAccount, 'Invalid account'),
});
// Client-side form in src/components/TransferForm.tsx
const { register } = useForm({ resolver: zodResolver(transferSchema) });
// Server-side API in src/app/api/transfers/route.ts
export async function POST(req) {
const data = transferSchema.parse(await req.json());
}