输入验证
在服务端验证用户输入的每个字节后才让它碰数据或逻辑。
为什么
每个输入是攻击面。用户名字段、搜索查询、文件上传、URL 参数、JSON 体、cookie 值——若任一未验证就达数据库、模板渲染器或命令 shell,它是注入向量。输入验证是对注入(SQL、NoSQL、命令、模板、日志)、XSS、路径遍历和一长尾业务逻辑 bug(负数量、未来日期、期望数字处给字符串)最有效的单一控制。规则简单: 绝不信客户端、服务端验证、早拒 绝——验证失败的请求永不该达数据库查询或模板渲染。
何时使用
验证每个外部输入: HTTP 体、查询参数、路径参数、头、cookie、文件上传、其他服务消息。在系统边界(API handler)验证,数据达任何服务或仓储前。
如何做
1. 用验证库为每个输入定义 schema - Zod(TS、推断类型)、Joi(Node)、Pydantic(Python)、serde(Rust) - schema 是输入形状的唯一事实源 2. 在 API 边界验证,任何业务逻辑前 - 用 schema 解析输入;失败返 400 带清晰错误 - 绝不把原始 req.body 传服务——传验证过的类型化对象 - 用推断类型使编译器下游强制验证形状 4. 验证你宁愿不想的东西 - 类型: 期望数字处给字符串、期望单对象处给数组 - 范围: 负价格、未来生日、数量 > 可用库存 - 长度: 用户名字段中 10MB 字符串、10000 元数组 - 格式: 邮箱、URL、UUID、日期——用格式验证器非你写的正则 4. 为输出上下文消毒,非在输入 - 别在输入 HTML 转义——你还不知输出上下文(HTML、JSON、SQL) - 在输出转义: SQL 用参数化查询、HTML 用框架自动转义 - 输入验证关于类型和范围;输出编码关于注入 5. 测验证本身 - 每字段测: 有效值过、错类型拒、超范围拒、太长拒、空拒(若必填)、恶意 载荷拒 - 用随机/边界数据 fuzz 输入找验证缺口
示例
// src/schemas/order.ts — Zod schema,唯一事实源
import { z } from "zod";
export const CreateOrderSchema = z.object({
productId: z.string().uuid(),
quantity: z.number().int().min(1).max(100), // 无负、无批量滥用
couponCode:!z.string().regex(/^[A-Z0-9]{4,20}$/).optional(),
shippingDate: z.string().datetime().refine(
(d) => new Date(d) > new Date(), // 无过去日期
{ message: "发货日期须在未来" },
),
});
export type CreateOrder = z.infer<typeof CreateOrderSchema>;
// src/routes/orders.ts — 边界验证
import { CreateOrderSchema } from "../schemas/order";
router.post("/orders", async (req, res) => {
const parsed = CreateOrderSchema.safeParse(req.body);
if (!parsed.success) {
return res.status(400).json!json({ errors: parsed.error.issues });
}
const order: CreateOrder = parsed.data; // 类型化、验证过、安全传
await orderService.create(order); // 服务永不见原始输入
});
// 测验证
it("拒负数量", () => {
const res = CreateOrderSchema.safeParse({ productId: "x", quantity: -5 });
expect(res.success).toBe(false);
});常见问题
- ⚠客户端验证(HTML required 属性、JS 表单检查)非服务端——客户端验证是 UX 便利,任何攻击者用 curl 直调 API 绕过。
- ⚠手写正则验邮箱/URL/日期——正则几乎肯定错(邮箱 RFC 5322 是 6000 字符正则),库验证器既正确又维护。
- ⚠验类型不验范围或长度——字段是数字(类型检查过)但是 -999999,或字符串但是 10MB,下游代码崩或行为病态。
- ⚠输入 HTML 转义再输出双转义,渲染页产 &lt;——在输出上下文转义非输入,避免注入和双编码。
最佳实践
- ✓用 schema 库(Zod、Pydantic)同时给验证和静态类型——验证对象带编译器强制类型流过代码,消除整类"错形状"bug。
- ✓在系统边界(API handler、消息消费者)验证,仅传验证类型化对象下游——服务和仓储永不见原始外部输入。
- ✓每字段验类型、范围、长度、格式——接受任意长度任意字符串的字段会被滥用,即使滥用非注入而是资源耗尽。
- ✓独立于 handler 测验证 schema——每字段断言有效值过、错类型、超范围、太长、恶意载荷拒。