Initial commit

This commit is contained in:
Zhongwei Li
2025-11-29 18:22:35 +08:00
commit 6fcffca9b0
35 changed files with 8235 additions and 0 deletions

View File

@@ -0,0 +1,611 @@
---
name: sanitizing-user-inputs
description: Sanitizing and validating user input to prevent XSS, injection attacks, and security vulnerabilities in TypeScript applications
---
# Security: Input Validation and Sanitization
**Purpose:** Prevent security vulnerabilities by properly validating and sanitizing all user input, preventing XSS, SQL injection, command injection, and other attack vectors.
**When to use:** Any time you process user input, API requests, URL parameters, form data, file uploads, or any external data source.
## Core Security Principles
### 1. Never Trust User Input
**All user input is potentially malicious until proven safe.**
This includes:
- Form submissions
- URL query parameters
- Request headers
- Cookies
- File uploads
- WebSocket messages
- GraphQL queries
- API request bodies
### 2. Validate Then Sanitize
**Two-step process:**
1. **Validation:** Check if input matches expected format/type
2. **Sanitization:** Remove or escape dangerous characters
**Never sanitize without validating first** - sanitization can hide malicious patterns.
### 3. Allowlist Over Blocklist
**Allowlist (good):** Accept only known-safe inputs
```typescript
const validRoles = ['admin', 'user', 'guest'] as const;
if (!validRoles.includes(role)) {
throw new ValidationError('Invalid role');
}
```
**Blocklist (bad):** Try to block known-bad inputs
```typescript
if (input.includes('<script>') || input.includes('DROP')) {
throw new Error('Suspicious input');
}
```
Blocklists are always incomplete. Attackers find new bypass techniques.
## Input Validation with Zod
### Basic Pattern
```typescript
import { z } from 'zod';
const UserInputSchema = z.object({
username: z.string()
.min(3, 'Username must be at least 3 characters')
.max(20, 'Username must be at most 20 characters')
.regex(/^[a-zA-Z0-9_]+$/, 'Username can only contain letters, numbers, and underscores'),
email: z.string().email('Invalid email format'),
age: z.number().int().positive().max(120),
});
type UserInput = z.infer<typeof UserInputSchema>;
function handleUserRegistration(input: unknown): UserInput {
return UserInputSchema.parse(input);
}
```
**Key points:**
- Input type is `unknown` (never trust it)
- Schema enforces exact constraints (allowlist)
- Parse throws on invalid input (fail-fast)
- Return type is fully typed after validation
### Common Validation Patterns
**Email validation:**
```typescript
const email = z.string().email().toLowerCase();
```
**Phone number (US format):**
```typescript
const phone = z.string().regex(/^\+1[0-9]{10}$/);
```
**URL validation:**
```typescript
const url = z.string().url();
```
**Enum values (allowlist):**
```typescript
const role = z.enum(['admin', 'user', 'guest']);
```
**Password requirements:**
```typescript
const password = z.string()
.min(12, 'Password must be at least 12 characters')
.regex(/[A-Z]/, 'Must contain uppercase letter')
.regex(/[a-z]/, 'Must contain lowercase letter')
.regex(/[0-9]/, 'Must contain number')
.regex(/[^A-Za-z0-9]/, 'Must contain special character');
```
**File upload validation:**
```typescript
const fileUpload = z.object({
filename: z.string()
.regex(/^[a-zA-Z0-9_\-\.]+$/, 'Invalid filename'),
mimetype: z.enum(['image/jpeg', 'image/png', 'image/gif']),
size: z.number().max(5 * 1024 * 1024, 'File too large (max 5MB)'),
});
```
## XSS Prevention
### Understanding XSS
**Cross-Site Scripting (XSS)** injects malicious JavaScript into your application:
```typescript
const userInput = '<script>alert("XSS")</script>';
document.innerHTML = userInput;
```
This executes the script in the user's browser, potentially:
- Stealing cookies/session tokens
- Performing actions as the user
- Defacing the page
- Redirecting to phishing sites
### Preventing XSS in TypeScript/React
**React (safe by default):**
```typescript
function UserProfile({ username }: { username: string }) {
return <div>{username}</div>;
}
```
React automatically escapes `{username}`. This is safe even if `username` contains `<script>`.
**Unsafe (DO NOT DO):**
```typescript
function UnsafeComponent({ html }: { html: string }) {
return <div dangerouslySetInnerHTML={{ __html: html }} />;
}
```
**If you must render HTML, sanitize it first:**
```typescript
import DOMPurify from 'isomorphic-dompurify';
function SafeHTMLComponent({ html }: { html: string }) {
const clean = DOMPurify.sanitize(html, {
ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'a'],
ALLOWED_ATTR: ['href'],
});
return <div dangerouslySetInnerHTML={{ __html: clean }} />;
}
```
**DOMPurify** removes dangerous tags and attributes:
- Removes `<script>`, `<iframe>`, `<object>`
- Removes event handlers (`onclick`, `onerror`)
- Removes `javascript:` URLs
- Configurable allowlist of safe tags/attributes
### Server-Side Rendering (SSR) Safety
**Express/Node.js:**
```typescript
import express from 'express';
import { escape } from 'html-escaper';
app.get('/profile', (req, res) => {
const username = escape(req.query.username as string);
res.send(`
<!DOCTYPE html>
<html>
<body>
<h1>Welcome ${username}</h1>
</body>
</html>
`);
});
```
**`html-escaper`** converts dangerous characters:
- `<``&lt;`
- `>``&gt;`
- `"``&quot;`
- `'``&#x27;`
- `&``&amp;`
## SQL Injection Prevention
### Understanding SQL Injection
**Vulnerable code:**
```typescript
const userId = req.params.id;
const query = `SELECT * FROM users WHERE id = ${userId}`;
db.query(query);
```
**Attack:**
```
GET /users/1; DROP TABLE users; --
```
Results in:
```sql
SELECT * FROM users WHERE id = 1; DROP TABLE users; --
```
### Safe Parameterized Queries
**Use parameterized queries (prepared statements):**
```typescript
import { Pool } from 'pg';
const pool = new Pool();
async function getUser(userId: string) {
const query = 'SELECT * FROM users WHERE id = $1';
const result = await pool.query(query, [userId]);
return result.rows[0];
}
```
**Key points:**
- `$1` is a placeholder (not string concatenation)
- Database driver escapes parameters safely
- SQL injection is impossible
**TypeScript type safety with query builders:**
```typescript
import { Kysely, PostgresDialect } from 'kysely';
interface Database {
users: {
id: string;
email: string;
};
}
const db = new Kysely<Database>({
dialect: new PostgresDialect({ pool }),
});
async function getUser(userId: string) {
return await db
.selectFrom('users')
.where('id', '=', userId)
.selectAll()
.executeTakeFirst();
}
```
**Benefits:**
- Type-safe queries
- Automatic parameterization
- Compile-time checking
### ORM Safety
**TypeORM:**
```typescript
import { getRepository } from 'typeorm';
async function getUser(userId: string) {
return await getRepository(User).findOne({
where: { id: userId },
});
}
```
**Prisma:**
If preventing SQL injection with Prisma ORM, use the preventing-sql-injection skill from prisma-6 for comprehensive $queryRaw vs $queryRawUnsafe guidance and parameterization strategies.
## Command Injection Prevention
### Understanding Command Injection
**Vulnerable code:**
```typescript
import { exec } from 'child_process';
function processFile(filename: string) {
exec(`convert ${filename} output.jpg`, (error, stdout) => {
console.log(stdout);
});
}
```
**Attack:**
```
filename = "input.jpg; rm -rf /"
```
Results in:
```bash
convert input.jpg; rm -rf / output.jpg
```
### Safe Command Execution
**Use array-based execution:**
```typescript
import { execFile } from 'child_process';
function processFile(filename: string) {
const FileNameSchema = z.string().regex(/^[a-zA-Z0-9_\-\.]+$/);
const validFilename = FileNameSchema.parse(filename);
execFile('convert', [validFilename, 'output.jpg'], (error, stdout) => {
if (error) {
throw error;
}
console.log(stdout);
});
}
```
**Key differences:**
- `execFile` (safe) vs `exec` (unsafe)
- Arguments passed as array, not string concatenation
- Shell is not invoked, no command injection possible
- Validate filename format first
## Path Traversal Prevention
### Understanding Path Traversal
**Vulnerable code:**
```typescript
import { readFile } from 'fs/promises';
async function getFile(filename: string) {
return await readFile(`./uploads/${filename}`, 'utf-8');
}
```
**Attack:**
```
filename = "../../etc/passwd"
```
Results in reading:
```
./uploads/../../etc/passwd
```
### Safe Path Handling
```typescript
import { readFile } from 'fs/promises';
import path from 'path';
async function getFile(filename: string) {
const FileNameSchema = z.string()
.regex(/^[a-zA-Z0-9_\-\.]+$/, 'Invalid filename');
const validFilename = FileNameSchema.parse(filename);
const uploadDir = path.resolve('./uploads');
const fullPath = path.resolve(uploadDir, validFilename);
if (!fullPath.startsWith(uploadDir)) {
throw new Error('Path traversal detected');
}
return await readFile(fullPath, 'utf-8');
}
```
**Protection layers:**
1. Validate filename format (no `..`, no `/`)
2. Resolve absolute paths
3. Check final path is within expected directory
## API Request Validation
### Express/Node.js Example
```typescript
import express from 'express';
import { z } from 'zod';
const CreateUserSchema = z.object({
username: z.string().min(3).max(20),
email: z.string().email(),
age: z.number().int().positive().max(120),
});
app.post('/users', async (req, res) => {
try {
const validData = CreateUserSchema.parse(req.body);
const user = await createUser(validData);
res.json(user);
} catch (error) {
if (error instanceof z.ZodError) {
res.status(400).json({
error: 'Validation failed',
details: error.errors,
});
} else {
res.status(500).json({ error: 'Internal server error' });
}
}
});
```
### Query Parameter Validation
```typescript
const QuerySchema = z.object({
page: z.coerce.number().int().positive().default(1),
limit: z.coerce.number().int().positive().max(100).default(20),
sort: z.enum(['asc', 'desc']).default('asc'),
});
app.get('/users', (req, res) => {
const params = QuerySchema.parse(req.query);
const users = await getUsers(params);
res.json(users);
});
```
**`z.coerce.number()`** converts string query params to numbers safely.
## Header Validation
### Accept Only Expected Headers
```typescript
const HeadersSchema = z.object({
'content-type': z.literal('application/json'),
'x-api-key': z.string().uuid(),
});
app.use((req, res, next) => {
try {
HeadersSchema.parse(req.headers);
next();
} catch (error) {
res.status(400).json({ error: 'Invalid headers' });
}
});
```
### CSRF Token Validation
```typescript
import csrf from 'csurf';
const csrfProtection = csrf({ cookie: true });
app.post('/transfer', csrfProtection, (req, res) => {
res.json({ success: true });
});
```
## Rate Limiting
### Prevent Abuse and DoS
```typescript
import rateLimit from 'express-rate-limit';
const limiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 100,
message: 'Too many requests, please try again later.',
});
app.use('/api/', limiter);
```
## Content Security Policy (CSP)
### Prevent XSS via Headers
```typescript
import helmet from 'helmet';
app.use(helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'unsafe-inline'"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", 'data:', 'https:'],
},
}));
```
This prevents loading scripts from untrusted sources.
## Common Mistakes
### Mistake 1: Client-Side Validation Only
```typescript
function LoginForm() {
const handleSubmit = (e) => {
if (email.includes('@')) {
}
};
}
```
**Problem:** Client-side validation can be bypassed. Always validate on server.
### Mistake 2: Regex Bypasses
```typescript
if (input.match(/^[a-z]+$/)) {
}
```
**Problem:** `^` and `$` match line start/end, not string start/end. Use `\A` and `\z` or Zod.
### Mistake 3: String Blacklists
```typescript
const sanitized = input.replace('<script>', '');
```
**Attack:** `<scr<script>ipt>` becomes `<script>` after replacement.
**Solution:** Use proper sanitization libraries like DOMPurify.
## Security Checklist
Before deploying any endpoint that processes user input:
- [ ] All input validated with Zod schemas
- [ ] Allowlist validation (not blocklist)
- [ ] HTML escaped or sanitized with DOMPurify
- [ ] SQL queries parameterized (never concatenated)
- [ ] File operations validate paths (prevent traversal)
- [ ] Command execution uses array arguments (not shell)
- [ ] Rate limiting enabled
- [ ] CSRF protection on state-changing endpoints
- [ ] Content Security Policy headers set
- [ ] Error messages don't leak sensitive info
## Related Skills
**Zod v4 Validation:**
- If validating common input formats, use the validating-string-formats skill for top-level string format functions (z.email(), z.uuid(), z.url(), z.ipv4(), z.base64())
- If transforming user input strings, use the transforming-string-methods skill for built-in string transformations (trim, toLowerCase, toUpperCase)
- If constructing Zod schemas for user input, use the validating-schema-basics skill for comprehensive schema patterns
**Prisma 6 Security:**
- If preventing SQL injection with Prisma 6, use the preventing-sql-injection skill from prisma-6 for database-specific sanitization patterns, $queryRaw vs $queryRawUnsafe parameterization, and safe query construction
- If validating inputs for Prisma database operations with type-safe Zod schemas, use the validating-query-inputs skill from prisma-6 for Prisma-specific validation patterns.
## Resources
- [OWASP Top 10](https://owasp.org/www-project-top-ten/)
- [OWASP Cheat Sheet Series](https://cheatsheetseries.owasp.org/)
- [Zod Documentation](https://zod.dev/)
- [DOMPurify](https://github.com/cure53/DOMPurify)
## Summary
**Input validation is your first and most critical line of defense.**
1. **Validate everything** - Never trust user input
2. **Use Zod** - Type-safe validation at runtime
3. **Sanitize HTML** - Use DOMPurify for any user-provided HTML
4. **Parameterize SQL** - Never concatenate queries
5. **Validate paths** - Prevent directory traversal
6. **Use allowlists** - More secure than blocklists
7. **Validate server-side** - Client validation is UX, not security
Security is not optional. Every unvalidated input is a potential breach.