Standards Violation: Missing Input Validation
Summary
This PR adds a new user profile update endpoint to the API gateway. The route handler directly passes req.body to the service layer with no input validation, no schema enforcement, and no type checking. Per our engineering standards (Rule #4: All inbound request bodies must be validated against a schema before processing), this PR is not ready to merge. The code changes look identical between the diff header and footer because the PR doesn't include the validation layer that the standards require. This is a merge-block — the endpoint cannot go live without schema validation.
Findings
Rule #4 of the engineering standards states: "All inbound request bodies must be validated against a schema before any business logic is executed. Unvalidated input must never reach the service layer."
This endpoint receives a raw req.body and passes it directly to updateUserProfile(userId, updates). No schema check, no type guard, no field whitelist. If the service method expects { name: string, email: string } but receives { name: 123, admin: true }, the service will either throw at runtime or — worse — silently accept malformed data and write it to the database.
Required fields in the request body schema should include at minimum: name (string, 1–100 chars), email (valid email format), and optionally avatar_url (valid URL). Any other fields should be rejected with a 400 response and a clear error message listing the invalid fields.
Use a schema validation library (Zod, Joi, or Valibot) — the choice is documented in the codebase standards. Zod is used elsewhere in this repo (see src/validators/user.ts), so use Zod for consistency.
The route accesses req.params.userId from the URL path. If a user can update any other user's profile by changing the URL parameter, this is a horizontal privilege escalation vulnerability — a standard user can modify another user's profile. The service layer needs to verify that the authenticated user (from the session/JWT) has permission to update the target user's profile. Typically this means checking that the authenticated user is either the same as userId or has an admin role.
If req.user is populated by the auth middleware, the check should be: if (req.user.id !== userId && !req.user.isAdmin) return res.status(403).json(...). If the auth middleware doesn't exist yet, that needs to be addressed before this endpoint goes live.
New write endpoints should be rate-limited. User profile updates from the same IP should be capped at, say, 10 requests per minute per user. Without rate limiting, an attacker can brute-force profile updates for any user ID (see Finding #2) and also use this endpoint as part of a broader abuse campaign. Check whether the existing rate limiter middleware (rateLimit from express-rate-limit) is applied to this route group — if not, it should be.
Profile changes are business-significant events. Who changed their name and when? This information matters for compliance, support tickets, and fraud detection. The service layer should emit a structured event (or write to an audit log table) recording: actor user ID, target user ID, fields changed, timestamp. If the event infrastructure doesn't exist yet, at minimum log these fields with structured logging at INFO level. This is not blocking for the PR but should be tracked as a follow-up.
Recommendation
This PR is blocked. Findings #1 and #2 are non-negotiable before this endpoint can serve production traffic. The good news: the endpoint structure is correct, the service call is clean, and the response format follows the established pattern. Adding validation and authorization will take approximately 20 lines of code — it's a small addition, not a rewrite.
Steps to unblock:
- Add Zod schema validation (or mirror the pattern in
src/validators/user.ts) and validatereq.bodybefore calling the service. - Add the authorization check — verify
req.user.id === userId || req.user.isAdmin. - Ensure the rate limiter middleware covers this route.
Once Findings #1 and #2 are addressed, this PR is ready to merge. Findings #3 and #4 are important but can be tracked as follow-up issues.
Suggested Fix
import { Router } from 'express'; import { z } from 'zod'; import { updateUserProfile } from '../services/user-service'; const ProfileUpdateSchema = z.object({ name: z.string().min(1).max(100), email: z.string().email(), avatar_url: z.string().url().optional(), }).strict(); // reject unknown keys const router = Router(); router.put('/users/:userId/profile', async (req, res) => { // Authorization: only the user or an admin can update this profile if (!req.user || (req.user.id !== req.params.userId && !req.user.isAdmin)) { return res.status(403).json({ error: 'Forbidden' }); } // Schema validation — Rule #4 const parsed = ProfileUpdateSchema.safeParse(req.body); if (!parsed.success) { return res.status(400).json({ error: 'Invalid request body', details: parsed.error.flatten(), }); } const result = await updateUserProfile(req.params.userId, parsed.data); res.json({ success: true, data: result }); }); export default router;
Why Codio flagged this
Codio maintains a standards.md document for every project it works in. When it reviews a PR, it cross-references the changed code against the relevant rules. Rule #4 — validate inbound request bodies — is a baseline security requirement. This PR added a new write endpoint with no validation, which directly violates that rule. Codio catches this pattern and blocks the merge with a specific reference to the rule and a concrete fix. Standards compliance isn't about being pedantic — unvalidated request bodies are the entry point for a wide range of attacks.
Get Codio reviewing your PRs
Ship reviews like this on every pull request. No reminders. No missed PRs.