# Client-side validation matches server-side

- **Pattern:** `ab-001448` (`finserv-form-validation.calculation-accuracy.client-server-parity`)
- **Severity:** low
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-001448
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-001448)

## Why it matters

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.

## Severity rationale

Low because mismatched constraints require an attacker to bypass client validation, which is trivial but limits casual exploitation to motivated users.

## Remediation

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.

```ts
// 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());
```

## Detection

- **ID:** `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`:**
  ```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());
  }
  ```

## External references

- cwe CWE-602
- owasp A03

Taxons: injection-and-input-trust, data-integrity

HTML version: https://auditbuffet.com/patterns/ab-001448
