白名单校验必须在服务端实现,需结合编码标准化、语义化字段规则与专用校验库;整数参数须强转并严格类型验证;字符串需按存储/查询上下文差异化清洗;规则须与数据库定义严格对齐并版本化管理。

白名单校验必须在服务端做,且不能只靠正则
客户端的 JavaScript 校验或 HTML pattern 属性毫无防护意义——攻击者绕过浏览器只需一条 curl 命令。真正起作用的白名单必须落在服务端入口,且不能仅依赖简单正则(比如 /^[a-zA-Z0-9_]+$/),因为:
- 多字节字符(如 UTF-8 编码的零宽空格)可能绕过 ASCII 范围正则
- 某些合法业务字段需支持连字符、点号、@ 符号(如邮箱、域名),但又不能放行单引号或分号
- 正则无法表达“长度+字符集+结构”的组合约束(例如“用户名必须 3–20 位,首字符为字母,其余为字母数字或下划线”)
建议用结构化校验函数替代单条正则:
- 先统一做
utf8mb4编码标准化(避免变体编码绕过) - 再按字段语义拆解规则:类型(字符串/整数/邮箱)、长度、允许字符集、是否允许空格、是否需转义输出
- 最后调用专用校验库(如 Python 的
voluptuous、Java 的javax.validation)而非手写if堆砌
整数类参数必须强制类型转换,别信 is_numeric()
PHP 的 is_numeric("123e4") 返回 true,但数据库里它会被当科学计数法解析;JavaScript 的 parseInt("0x1F") 会转成十进制 31,而 MySQL 可能直接执行十六进制字面量。这些都不是注入,但属于**隐式语义漂移**,可能引发逻辑错误甚至被利用。
正确做法是:
- PHP:用
(int)强转 +filter_var($id, FILTER_VALIDATE_INT)双重确认 - Java:用
Integer.parseInt()并捕获NumberFormatException,拒绝所有非纯数字字符串(含开头空格、+号) - Python:用
int(user_input)并捕获ValueError,不接受0x、0b、浮点格式
关键点:失败必须返回 400 错误,而不是静默截断或默认值——静默处理等于给攻击者提供试探反馈。
字符串字段要区分“存储”和“查询”两种白名单
同一个字段在不同上下文需要不同清洗策略。例如用户昵称:
- 存储时可允许 emoji、中文、短横线(需 UTF8MB4 支持)
- 但用于 WHERE username = ? 查询时,若后端用了 LIKE 模糊匹配,就得额外过滤通配符 % 和 _
- 若用于 ORDER BY 动态列名,则必须严格限制为预定义枚举值(["created_at", "score"]),绝不能放行任意字符串
常见错误:
- 把邮箱校验规则直接套用到搜索关键词上(邮箱需
@,搜索词不需要) - 对 URL 参数中的
sort字段只做“非空判断”,没限定可选值范围 - 使用
addslashes()或mysql_real_escape_string()替代参数化查询(已废弃且不可靠)
白名单规则必须和数据库字段定义对齐
如果数据库中 user.phone 是 VARCHAR(11),但白名单允许 15 位数字,那多出来的 4 位就可能成为截断攻击入口(尤其配合宽字节注入时)。更隐蔽的问题是:
- MySQL TINYINT 实际存 -128 到 127,但应用层若只校验“是否为整数”,就可能把 128 当作合法值传入,触发隐式转换或报错泄露信息
- PostgreSQL 的 ENUM 类型字段,白名单必须精确匹配枚举值列表,不能只校验“是否为字符串”
实操建议:
- 自动生成白名单规则:从数据库 schema 中提取字段类型、长度、是否允许 NULL、是否有 CHECK 约束,反向生成校验逻辑
- 在 CI 流程中加入比对检查:当 DDL 变更(如
ALTER TABLE ... MODIFY phone VARCHAR(15)),自动触发白名单规则更新告警 - 所有白名单配置必须版本化管理,禁止运行时热修改(避免环境不一致)
真实世界里最常被忽略的,是字段语义和校验粒度的错位——不是规则不够严,而是规则没对准真正的攻击面。











