DEV Community

Atlas Whoff
Atlas Whoff

Posted on

Input Sanitization vs Validation: Protecting Your API From Injection

Input Sanitization vs Validation: Protecting Your API From Injection

Validation and sanitization are different things. Mixing them up creates security holes.

The Distinction

Validation: checking that input meets your requirements. Reject if it doesn't.

Sanitization: transforming input to make it safe. Modify and accept.

Rule of thumb: validate at your API boundary, sanitize before rendering.

SQL Injection

// VULNERABLE: string interpolation in SQL
const users = await db.query(
  `SELECT * FROM users WHERE email = '${req.body.email}'`
);
// If email = "' OR '1'='1", this returns ALL users

// SAFE: parameterized queries (Prisma handles this automatically)
const users = await prisma.user.findMany({
  where: { email: req.body.email }, // Always parameterized
});

// SAFE: raw queries with parameters
const users = await prisma.$queryRaw`
  SELECT * FROM users WHERE email = ${req.body.email}
`;
Enter fullscreen mode Exit fullscreen mode

XSS: Cross-Site Scripting

// VULNERABLE: rendering user input as HTML
const html = `<div>${userComment}</div>`; // If comment = '<script>alert(1)</script>'

// SAFE: escape HTML entities
import DOMPurify from 'dompurify';

// For React: JSX escapes by default
return <div>{userComment}</div>; // Safe  React escapes automatically

// For dangerouslySetInnerHTML: always sanitize first
return (
  <div
    dangerouslySetInnerHTML={{
      __html: DOMPurify.sanitize(userComment)
    }}
  />
);
Enter fullscreen mode Exit fullscreen mode

Path Traversal

import path from 'path';

// VULNERABLE: using user input in file paths directly
const file = fs.readFileSync(`/uploads/${req.params.filename}`);
// If filename = '../../../etc/passwd', you've exposed your server

// SAFE: resolve and validate the path stays in allowed directory
const uploadsDir = '/uploads';
const requestedPath = path.resolve(uploadsDir, req.params.filename);

if (!requestedPath.startsWith(uploadsDir)) {
  return res.status(403).json({ error: 'Forbidden' });
}

const file = fs.readFileSync(requestedPath);
Enter fullscreen mode Exit fullscreen mode

Prototype Pollution

// VULNERABLE: merging user input into objects
function merge(target: object, source: object) {
  for (const key in source) {
    target[key] = source[key]; // '__proto__' key pollutes Object.prototype
  }
}

// SAFE: use Zod to strip unknown keys before processing
const SafeSchema = z.object({
  name: z.string(),
  value: z.number(),
  // Unknown keys are stripped by default in Zod
});

const safe = SafeSchema.parse(userInput);
Enter fullscreen mode Exit fullscreen mode

Command Injection

import { execFile } from 'child_process';

// VULNERABLE: shell=true with user input
exec(`convert ${userFile} output.jpg`); // Never do this

// SAFE: execFile with array arguments (no shell)
execFile('convert', [userFile, 'output.jpg'], (err, stdout) => {
  // Arguments are passed directly, not through shell
});
Enter fullscreen mode Exit fullscreen mode

Security hardening — injection prevention, XSS protection, input validation — is production-configured in the AI SaaS Starter Kit. The MCP Security Scanner audits MCP servers for these same vulnerabilities.

Top comments (0)