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';
// Define strict schemas
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']),
});
// Validate at API boundary
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; // Type-safe, validated
await createUser(validated);
});
Path Traversal Prevention
BAD:
app.get('/files/:filename', (req, res) => {
const file = path.join('/uploads', req.params.filename);
res.sendFile(file); // ../../etc/passwd works!
});
GOOD:
import path from 'path';
const UPLOAD_DIR = path.resolve('/uploads');
app.get('/files/:filename', (req, res) => {
const filename = path.basename(req.params.filename); // Strip directory traversal
const filePath = path.resolve(UPLOAD_DIR, filename);
// Verify resolved path is still within allowed directory
if (!filePath.startsWith(UPLOAD_DIR)) {
return res.status(403).json({ error: 'Access denied' });
}
res.sendFile(filePath);
});
XSS Prevention
BAD:
// Server-side rendering with raw user input
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>`);
});
// Or better: use a template engine with auto-escaping (EJS, Handlebars, React)
File Upload Validation
import { fileTypeFromBuffer } from 'file-type';
const ALLOWED_TYPES = ['image/jpeg', 'image/png', 'image/webp'];
const MAX_SIZE = 5 * 1024 * 1024; // 5MB
async function validateUpload(file: Buffer, filename: string) {
// Check size
if (file.length > MAX_SIZE) {
throw new Error('File too large (max 5MB)');
}
// Check actual file type via magic bytes (not extension!)
const type = await fileTypeFromBuffer(file);
if (!type || !ALLOWED_TYPES.includes(type.mime)) {
throw new Error(`Invalid file type. Allowed: ${ALLOWED_TYPES.join(', ')}`);
}
// Generate safe filename (never use user-provided filename directly)
const safeFilename = `${crypto.randomUUID()}.${type.ext}`;
return safeFilename;
}
Command Injection Prevention
BAD:
import { exec } from 'child_process';
exec(`convert ${userFilename} output.png`); // Shell injection!
GOOD:
import { execFile } from 'child_process';
// execFile does NOT use shell, so injection is impossible
execFile('convert', [userFilename, 'output.png']);
Laravel / PHP
// Form request validation
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
# Usage in FastAPI
@app.post("/users")
async def create_user(user: CreateUser):
# user is already validated and type-safe
pass
Validation Checklist
For every API endpoint, verify:
- All input fields have type validation (string, number, boolean)
- String fields have max length limits
- Numeric fields have min/max range
- Enum fields use allowlists (not blocklists)
- Email fields use proper email validation
- URL fields validate protocol (https only) and domain
- File uploads check magic bytes, not just extension
- Path parameters cannot traverse directories
- No user input is passed to shell commands
- No user input is used in dynamic
import()oreval()
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.