← All examples / TypeScript / standards-violation-missing-validation
TypeScript Blocks Merge

Standards Violation: Missing Input Validation

Endpoint accepts raw request body without schema validation per standards.md Rule #4.

Repository api-gateway
PR Title Add user profile update endpoint
Author @tnakamura
Files changed 4
Diff size +89 / -21 lines
src/routes/user-profile.ts
import { Router } from 'express';
import { updateUserProfile } from '../services/user-service';
const router = Router();
- router.put('/users/:userId/profile', async (req, res) => {
- const { userId } = req.params;
- const updates = req.body;
- const result = await updateUserProfile(userId, updates);
- res.json({ success: true, data: result });
- });
+ router.put('/users/:userId/profile', async (req, res) => {
+ const { userId } = req.params;
+ const updates = req.body;
+
+ const result = await updateUserProfile(userId, updates);
+ res.json({ success: true, data: result });
+ });

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

Finding #1 — No Request Body Validation (Rule #4 Violation) Blocks Merge

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.

Finding #2 — No Authorization Check High

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.

Finding #3 — Missing Rate Limiting on New Endpoint Medium

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.

Finding #4 — No Audit Trail for Profile Updates Medium

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:

  1. Add Zod schema validation (or mirror the pattern in src/validators/user.ts) and validate req.body before calling the service.
  2. Add the authorization check — verify req.user.id === userId || req.user.isAdmin.
  3. 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

src/routes/user-profile.ts
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.

Join the waitlist →
← Previous N+1 Query in ORM Lookup Loop