3. 输入验证与数据清洗实战
输入验证是安全编码的第一道闸门,但它不是“过滤掉坏字符”这么简单。真正可靠的输入处理遵循一条核心原则:先按业务规则定义什么是合法输入,再拒绝一切不符合定义的数据。这就是白名单(allowlist)思路,与黑名单(denylist)相对。黑名单永远列举不全——攻击者可以用编码、大小写、Unicode 变体绕过;白名单只承认已知合法集合,天然更安全。
本节围绕一条完整的输入处理链展开:原始输入 → 类型转换 → 边界检查 → 格式白名单校验 → 数据清洗 → 安全使用。下面这张图展示了各环节的职责与失败时的处置方向。
类型转换与边界检查:先定类型,再定范围
很多漏洞源于“把字符串当数字用”。例如从 URL 取 ?id=1 OR 1=1,若直接拼进 SQL 就危险;若先做类型转换,非法值会立即暴露。以 Python 为例:
def parse_age(raw: str) -> int:
try:
age = int(raw) # 类型转换:非数字直接抛异常
except (TypeError, ValueError):
raise ValueError("年龄必须为整数")
if not (0 <= age <= 150): # 边界检查:闭区间
raise ValueError("年龄超出合理范围")
return age
这里有两个关键点:类型转换失败要显式拒绝,不能默认给 0 或 None 继续执行;边界必须用业务上真实的上下限,而不是随手写 0 到 999999。边界检查还要注意整数溢出、浮点精度、负数、空值等边界情况。对于金额,建议用 Decimal 而非 float,避免 0.1+0.2 的精度陷阱。
正则表达式安全:既要匹配对,也要防 ReDoS
正则常用于格式白名单,例如用户名只允许字母数字下划线、长度 3–20:
^[A-Za-z0-9_]{3,20}$
但正则本身也可能成为攻击面。嵌套量词如 (a+)+$ 在遇到长串不匹配输入时会产生指数级回溯,导致 ReDoS(正则拒绝服务)。防护要点:
- 避免嵌套量词和重叠可选分支,如
(a|a)*。 - 对输入长度先做上限截断,再交给正则。
- 使用支持线性匹配的引擎或设置超时。
- 能用字符串枚举、集合判断的,不要用复杂正则。
另外,正则中的 . 默认不匹配换行,若用于校验多行内容需显式处理;^ 和 $ 在部分语言中会匹配行首行尾,应使用 \A 和 \z 锚定整个字符串,防止 abc\n恶意内容 绕过。
文件上传与路径遍历防护
文件上传是输入验证中最容易出问题的场景。攻击者可能上传 WebShell、覆盖系统文件,或通过文件名中的 ../ 把文件写到任意目录。防护要分层:
- 扩展名白名单:只允许业务需要的类型,如
.jpg .png .pdf,并统一转小写比较。 - MIME 与魔数校验:读取文件头字节判断真实类型,不能只信客户端传来的 Content-Type。
- 重命名存储:用随机 UUID 作为文件名,丢弃用户原始文件名,从根源消除路径注入。
- 路径规范化:若必须使用用户提供的路径片段,先
os.path.realpath规范化,再确认结果仍位于允许的基目录内。 - 存储目录禁用执行权限:Web 服务器不解析上传目录中的脚本。
import os, uuid
BASE = "/var/app/uploads"
def safe_save(filename: str, data: bytes) -> str:
ext = os.path.splitext(filename)[1].lower()
if ext not in {".jpg", ".png", ".pdf"}:
raise ValueError("不允许的文件类型")
name = f"{uuid.uuid4().hex}{ext}"
path = os.path.realpath(os.path.join(BASE, name))
if not path.startswith(os.path.realpath(BASE) + os.sep):
raise ValueError("路径越界")
with open(path, "wb") as f:
f.write(data)
return path
../../etc/passwd,URL 编码后为 %2e%2e%2f。仅做字符串替换 ../ 不够,因为编码、双写、绝对路径都可能绕过。正确做法是规范化后做前缀校验,而不是黑名单替换。
数据清洗与输出编码的分工
需要区分两个概念:输入验证决定数据能否进入系统,输出编码决定数据如何安全地呈现。输入验证不能替代输出编码。例如用户昵称允许包含 <,存储时保留原样,渲染到 HTML 时再转义为 <。如果输入阶段就粗暴删除所有特殊字符,会破坏合法数据(如 O'Brien 中的撇号)。
清洗的常见操作包括:去除首尾空白、统一 Unicode 归一化(NFC/NFKC)、限制长度、去除控制字符。但清洗要谨慎,不能改变业务语义。例如密码不应被 trim 或截断,否则用户设置的密码与登录时输入不一致。
把上述环节串起来,就形成了一条可复用的输入处理管线。工程实践中,建议把校验规则集中定义(如用 schema 或校验库),避免散落在各处导致遗漏;同时为每个输入字段编写边界测试用例,覆盖空值、超长、特殊字符、编码变体和类型错误,用自动化测试保证规则不被后续修改破坏。