Skip to content
返回质量保障

输入验证

在服务端验证用户输入的每个字节后才让它碰数据或逻辑。

为什么

每个输入是攻击面。用户名字段、搜索查询、文件上传、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 转义再输出双转义,渲染页产 &amp;lt;——在输出上下文转义非输入,避免注入和双编码。

最佳实践

  • ✓用 schema 库(Zod、Pydantic)同时给验证和静态类型——验证对象带编译器强制类型流过代码,消除整类"错形状"bug。
  • ✓在系统边界(API handler、消息消费者)验证,仅传验证类型化对象下游——服务和仓储永不见原始外部输入。
  • ✓每字段验类型、范围、长度、格式——接受任意长度任意字符串的字段会被滥用,即使滥用非注入而是资源耗尽。
  • ✓独立于 handler 测验证 schema——每字段断言有效值过、错类型、超范围、太长、恶意载荷拒。