Skip to content

refactor: replace security-validation.ts helpers with Zod schemas and a proper sanitization library #442

@coderabbitai

Description

@coderabbitai

Overview

src/server/utils/security-validation.ts contains a collection of ad-hoc runtime validation and sanitization helpers that duplicate functionality already provided by Zod (used at every tRPC boundary) or that apply insufficient security controls. This issue tracks replacing each function with the idiomatic, officially-recommended approach.

References:


Functions & recommended replacements

1. validatePagination

Current problem: Duplicates pagination constraints that Zod can express declaratively. Called in many routers after the input has already been parsed by a Zod schema, making it a redundant second pass.

Recommendation: Express pagination constraints directly in each router's Zod input schema:

page: z.number().int().min(1).default(1),
limit: z.number().int().min(1).max(100).default(20),

This is consistent with the pattern already introduced in the updated GetPcListingReportsSchema in src/schemas/pcListing.ts. All routers (games.ts, listingReports.ts, pcListingReports.ts, pcListings/core.ts, listings/core.ts) should migrate to schema-level constraints and stop calling validatePagination.


2. validateEnum

Current problem: Called inside router procedure handlers after tRPC/Zod has already parsed and validated the input object. Any enum field that reaches this guard has already passed schema validation, so the check is a no-op under correct usage and masks a real schema gap if it is ever reached.

Recommendation: Move enum validation into the Zod input schema using z.nativeEnum() or z.enum([...]):

reason: z.nativeEnum(ReportReason),
status: z.nativeEnum(ReportStatus),

Per the Next.js Data Security guide, "Action arguments [should be] validated in the action or inside the Data Access Layer" — the tRPC Zod schema is exactly that boundary.


3. sanitizeInput

Current problem: Strips only <, >, javascript:, and on*= patterns. OWASP's Input Validation Cheat Sheet explicitly states that "denylisting can be useful as an additional layer of defense to catch some common malicious patterns, [but] should not be relied upon as the primary method." This regex is trivially bypassed and gives false confidence.

In practice:

  • React already HTML-escapes all values rendered via JSX, preventing reflected XSS in the UI.
  • Prisma uses parameterized queries, preventing SQL injection.
  • For user-supplied text stored and later rendered as plain text, no sanitization is needed beyond what Prisma + React provide.
  • For any field that will be rendered as HTML (e.g. rich-text descriptions), use a library purpose-built for the job, such as sanitize-html or DOMPurify (JSDOM variant for Node).

Recommendation:

  1. Remove sanitizeInput calls from plain-text fields (notes, searchTerm, report descriptions) — React + Prisma cover those.
  2. If any field is genuinely rendered as HTML, introduce a dedicated server-side sanitizer (e.g. sanitize-html) scoped to that field only.
  3. Add a Content-Security-Policy header in next.config as a defence-in-depth layer per the OWASP CSP Cheat Sheet.

4. validateRequired

Current problem: Replicates what a non-optional Zod field already enforces. Currently used in user-pc-presets.repository.ts to assert that userId and name are present.

Recommendation: Ensure the repository is only called with data that has already been validated by a Zod schema (e.g. z.string().min(1)). If the repository must remain callable without a prior Zod pass, use TypeScript's type system (non-nullable types) so the compiler—rather than a runtime guard—enforces the constraint.


5. validateNonEmptyArray

Current problem: Duplicates z.array(z.string().uuid()).min(1) which Zod expresses in one line.

Recommendation: Replace the guard in games.ts with z.array(...).min(1) in the input schema for the bulkApprove/bulkReject procedures.


6. validateStringFormat + ValidationPatterns

Current problem: Used to assert UUID format for userId in user-pc-presets.repository.ts. Zod provides z.string().uuid() natively.

Recommendation: Validate UUID fields in the tRPC input schema (z.string().uuid()) so the error is surfaced at the API boundary, not deep inside the repository.


Suggested migration order

  1. Update all Zod input schemas to encode pagination, enum, UUID, and non-empty-array constraints directly.
  2. Remove all validatePagination, validateEnum, validateRequired, validateNonEmptyArray, validateStringFormat call sites.
  3. Audit every sanitizeInput call site: remove where React + Prisma are sufficient; replace with sanitize-html where HTML rendering occurs.
  4. Delete src/server/utils/security-validation.ts once no callers remain.
  5. Add a CSP header in next.config as a complementary layer.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions