| name | input-validation |
| description | Validate and sanitize all user input to prevent XSS, path traversal, command injection, and malformed data. Covers Zod, Joi, class-validator, Laravel validation, and Python Pydantic. Use when building forms, API endpoints, file uploads, or fixing injection vulnerabilities beyond SQL. |
| version | 1.0 |
Input Validation
Validate and sanitize ALL user input at every entry point. This covers Finding #2 (path traversal) and extends beyond SQL injection to XSS, command injection, and data integrity.
The Rule
NEVER trust user input. ALWAYS validate type, length, format, and range on the server side.
Attack Vectors Prevented
| Attack | How it works | Validation fix |
|---|
| XSS | Inject <script> into rendered HTML | HTML-encode output, validate no HTML in text fields |
| Path Traversal | ../../etc/passwd in file paths | Whitelist allowed paths, reject .. |
| Command Injection | ; rm -rf / in shell commands | Never pass user input to shell, use spawn with args array |
| SSRF | User-controlled URLs fetch internal services | Whitelist allowed domains, block private IPs |
| Prototype Pollution | __proto__ in JSON keys | Strip dangerous keys, use Object.create(null) |
| File Upload Abuse | .php file uploaded as "image" | Validate MIME type + magic bytes, not just extension |
Framework Patterns
Node.js with Zod (Recommended)
import { z } from 'zod';
const CreateUserSchema = z.object({
email: z.string().email().max(255),
name: z.string().min(1).max(100).regex(/^[a-zA-Z\s'-]+$/),
age: z.number().int().min(13).max(150),
role: z.enum(['user', 'admin']),
});
app.post('/users', async (req, res) => {
const result = CreateUserSchema.safeParse(req.body);
if (!result.success) {
return res.status(400).json({
error: 'Validation failed',
issues: result.error.issues.map(i => ({
field: i.path.join('.'),
message: i.message,
})),
});
}
const validated = result.data;
await createUser(validated);
});
Path Traversal Prevention
BAD:
app.get('/files/:filename', (req, res) => {
const file = path.join('/uploads', req.params.filename);
res.sendFile(file);
});
GOOD:
import path from 'path';
const UPLOAD_DIR = path.resolve('/uploads');
app.get('/files/:filename', (req, res) => {
const filename = path.basename(req.params.filename);
const filePath = path.resolve(UPLOAD_DIR, filename);
if (!filePath.startsWith(UPLOAD_DIR)) {
return res.status(403).json({ error: 'Access denied' });
}
res.sendFile(filePath);
});
XSS Prevention
BAD:
app.get('/profile', (req, res) => {
res.send(`<h1>Welcome, ${req.query.name}</h1>`);
});
GOOD:
import { encode } from 'html-entities';
app.get('/profile', (req, res) => {
const safeName = encode(req.query.name || '');
res.send(`<h1>Welcome, ${safeName}</h1>`);
});
File Upload Validation
import { fileTypeFromBuffer } from 'file-type';
const ALLOWED_TYPES = ['image/jpeg', 'image/png', 'image/webp'];
const MAX_SIZE = 5 * 1024 * 1024;
async function validateUpload(file: Buffer, filename: string) {
if (file.length > MAX_SIZE) {
throw new Error('File too large (max 5MB)');
}
const type = await fileTypeFromBuffer(file);
if (!type || !ALLOWED_TYPES.includes(type.mime)) {
throw new Error(`Invalid file type. Allowed: ${ALLOWED_TYPES.join(', ')}`);
}
const safeFilename = `${crypto.randomUUID()}.${type.ext}`;
return safeFilename;
}
Command Injection Prevention
BAD:
import { exec } from 'child_process';
exec(`convert ${userFilename} output.png`);
GOOD:
import { execFile } from 'child_process';
execFile('convert', [userFilename, 'output.png']);
Laravel / PHP
class StoreUserRequest extends FormRequest
{
public function rules(): array
{
return [
'email' => ['required', 'email:rfc,dns', 'max:255'],
'name' => ['required', 'string', 'max:100', 'regex:/^[a-zA-Z\s\'-]+$/'],
'age' => ['required', 'integer', 'min:13', 'max:150'],
'role' => ['required', Rule::in(['user', 'admin'])],
'avatar' => ['nullable', 'image', 'mimes:jpg,png,webp', 'max:5120'],
];
}
}
Python with Pydantic
from pydantic import BaseModel, EmailStr, Field, validator
import re
class CreateUser(BaseModel):
email: EmailStr
name: str = Field(min_length=1, max_length=100)
age: int = Field(ge=13, le=150)
role: Literal['user', 'admin']
@validator('name')
def name_must_be_safe(cls, v):
if not re.match(r"^[a-zA-Z\s'-]+$", v):
raise ValueError('Name contains invalid characters')
return v
@app.post("/users")
async def create_user(user: CreateUser):
pass
Validation Checklist
For every API endpoint, verify:
PDPA 2024 Consequence
Seksyen 5(1) dan 5(2) PDPA 2010 (Pindaan 2024):
- Denda sehingga RM1,000,000
- Penjara sehingga 3 tahun
Path traversal (Finding #2) allowed attackers to download the entire user database. The developer was charged because input validation is a basic security measure that any competent developer should implement.