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:
- Remove
sanitizeInput calls from plain-text fields (notes, searchTerm, report descriptions) — React + Prisma cover those.
- If any field is genuinely rendered as HTML, introduce a dedicated server-side sanitizer (e.g.
sanitize-html) scoped to that field only.
- 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
- Update all Zod input schemas to encode pagination, enum, UUID, and non-empty-array constraints directly.
- Remove all
validatePagination, validateEnum, validateRequired, validateNonEmptyArray, validateStringFormat call sites.
- Audit every
sanitizeInput call site: remove where React + Prisma are sufficient; replace with sanitize-html where HTML rendering occurs.
- Delete
src/server/utils/security-validation.ts once no callers remain.
- Add a CSP header in
next.config as a complementary layer.
Overview
src/server/utils/security-validation.tscontains 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.
validatePaginationCurrent 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:
This is consistent with the pattern already introduced in the updated
GetPcListingReportsSchemainsrc/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 callingvalidatePagination.2.
validateEnumCurrent 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()orz.enum([...]):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.
sanitizeInputCurrent problem: Strips only
<,>,javascript:, andon*=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:
sanitize-htmlorDOMPurify(JSDOM variant for Node).Recommendation:
sanitizeInputcalls from plain-text fields (notes,searchTerm, report descriptions) — React + Prisma cover those.sanitize-html) scoped to that field only.next.configas a defence-in-depth layer per the OWASP CSP Cheat Sheet.4.
validateRequiredCurrent problem: Replicates what a non-optional Zod field already enforces. Currently used in
user-pc-presets.repository.tsto assert thatuserIdandnameare 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.
validateNonEmptyArrayCurrent problem: Duplicates
z.array(z.string().uuid()).min(1)which Zod expresses in one line.Recommendation: Replace the guard in
games.tswithz.array(...).min(1)in the input schema for thebulkApprove/bulkRejectprocedures.6.
validateStringFormat+ValidationPatternsCurrent problem: Used to assert UUID format for
userIdinuser-pc-presets.repository.ts. Zod providesz.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
validatePagination,validateEnum,validateRequired,validateNonEmptyArray,validateStringFormatcall sites.sanitizeInputcall site: remove where React + Prisma are sufficient; replace withsanitize-htmlwhere HTML rendering occurs.src/server/utils/security-validation.tsonce no callers remain.next.configas a complementary layer.