Sanitizing and validating user input to prevent XSS, injection attacks, and security vulnerabilities in TypeScript applications
Validates all user input with Zod schemas to prevent XSS, SQL injection, command injection, and path traversal attacks. Triggers when processing form data, API requests, URL parameters, file uploads, or any external data sources.
/plugin marketplace add djankies/claude-configs/plugin install typescript@claude-configsThis skill inherits all available tools. When active, it can use any tool Claude has access to.
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.
All user input is potentially malicious until proven safe.
This includes:
Two-step process:
Never sanitize without validating first - sanitization can hide malicious patterns.
Allowlist (good): Accept only known-safe inputs
const validRoles = ['admin', 'user', 'guest'] as const;
if (!validRoles.includes(role)) {
throw new ValidationError('Invalid role');
}
Blocklist (bad): Try to block known-bad inputs
if (input.includes('<script>') || input.includes('DROP')) {
throw new Error('Suspicious input');
}
Blocklists are always incomplete. Attackers find new bypass techniques.
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:
unknown (never trust it)Email validation:
const email = z.string().email().toLowerCase();
Phone number (US format):
const phone = z.string().regex(/^\+1[0-9]{10}$/);
URL validation:
const url = z.string().url();
Enum values (allowlist):
const role = z.enum(['admin', 'user', 'guest']);
Password requirements:
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:
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)'),
});
Cross-Site Scripting (XSS) injects malicious JavaScript into your application:
const userInput = '<script>alert("XSS")</script>';
document.innerHTML = userInput;
This executes the script in the user's browser, potentially:
React (safe by default):
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):
function UnsafeComponent({ html }: { html: string }) {
return <div dangerouslySetInnerHTML={{ __html: html }} />;
}
If you must render HTML, sanitize it first:
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:
<script>, <iframe>, <object>onclick, onerror)javascript: URLsExpress/Node.js:
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:
< → <> → >" → "' → '& → &Vulnerable code:
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:
SELECT * FROM users WHERE id = 1; DROP TABLE users; --
Use parameterized queries (prepared statements):
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)TypeScript type safety with query builders:
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:
TypeORM:
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.
Vulnerable code:
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:
convert input.jpg; rm -rf / output.jpg
Use array-based execution:
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)Vulnerable code:
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
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:
.., no /)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' });
}
}
});
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.
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' });
}
});
import csrf from 'csurf';
const csrfProtection = csrf({ cookie: true });
app.post('/transfer', csrfProtection, (req, res) => {
res.json({ success: true });
});
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);
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.
function LoginForm() {
const handleSubmit = (e) => {
if (email.includes('@')) {
}
};
}
Problem: Client-side validation can be bypassed. Always validate on server.
if (input.match(/^[a-z]+$/)) {
}
Problem: ^ and $ match line start/end, not string start/end. Use \A and \z or Zod.
const sanitized = input.replace('<script>', '');
Attack: <scr<script>ipt> becomes <script> after replacement.
Solution: Use proper sanitization libraries like DOMPurify.
Before deploying any endpoint that processes user input:
Zod v4 Validation:
Prisma 6 Security:
Input validation is your first and most critical line of defense.
Security is not optional. Every unvalidated input is a potential breach.
This skill should be used when the user asks to "create an agent", "add an agent", "write a subagent", "agent frontmatter", "when to use description", "agent examples", "agent tools", "agent colors", "autonomous agent", or needs guidance on agent structure, system prompts, triggering conditions, or agent development best practices for Claude Code plugins.
This skill should be used when the user asks to "create a slash command", "add a command", "write a custom command", "define command arguments", "use command frontmatter", "organize commands", "create command with file references", "interactive command", "use AskUserQuestion in command", or needs guidance on slash command structure, YAML frontmatter fields, dynamic arguments, bash execution in commands, user interaction patterns, or command development best practices for Claude Code.
This skill should be used when the user asks to "create a hook", "add a PreToolUse/PostToolUse/Stop hook", "validate tool use", "implement prompt-based hooks", "use ${CLAUDE_PLUGIN_ROOT}", "set up event-driven automation", "block dangerous commands", or mentions hook events (PreToolUse, PostToolUse, Stop, SubagentStop, SessionStart, SessionEnd, UserPromptSubmit, PreCompact, Notification). Provides comprehensive guidance for creating and implementing Claude Code plugin hooks with focus on advanced prompt-based hooks API.