Back to skills
SkillHub ClubShip Full StackFull Stack

sanitizing-user-inputs

Imported from https://github.com/djankies/claude-configs.

Packaged view

This page reorganizes the original catalog entry around fit, installability, and workflow context first. The original raw source lives below.

Stars
0
Hot score
74
Updated
March 20, 2026
Overall rating
C2.6
Composite score
2.6
Best-practice grade
B70.4

Install command

npx @skill-hub/cli install djankies-claude-configs-sanitizing-user-inputs

Repository

djankies/claude-configs

Skill path: typescript/skills/sanitizing-user-inputs

Imported from https://github.com/djankies/claude-configs.

Open repository

Best for

Primary workflow: Ship Full Stack.

Technical facets: Full Stack.

Target audience: everyone.

License: Unknown.

Original source

Catalog source: SkillHub Club.

Repository owner: djankies.

This is still a mirrored public skill entry. Review the repository before installing into production workflows.

What it helps with

  • Install sanitizing-user-inputs into Claude Code, Codex CLI, Gemini CLI, or OpenCode workflows
  • Review https://github.com/djankies/claude-configs before adding sanitizing-user-inputs to shared team environments
  • Use sanitizing-user-inputs for development workflows

Works across

Claude CodeCodex CLIGemini CLIOpenCode

Favorites: 0.

Sub-skills: 0.

Aggregator: No.

Original source / Raw SKILL.md

---
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.
sanitizing-user-inputs | SkillHub