本文目录导读:

对用户输入进行有效过滤是保障系统安全、稳定和数据质量的关键环节,一个优秀的过滤策略应当遵循“纵深防御”的原则,结合客户端与服务端双重验证,并针对不同的应用场景(如SQL、HTML、命令行等)采用不同的技术。
以下是系统化的用户输入过滤方案,分为核心原则、关键方法、场景实践三个部分:
核心原则
- 绝不信任用户输入:这是安全的第一性原理,所有输入在验证通过前都应视为不可信的。
- 白名单优先:尽可能使用“允许列表”(只允许已知安全的字符或模式),而不是“黑名单”(试图阻止已知危险的字符),黑名单极易被绕过。
- 服务端过滤是必须的:客户端(前端)的过滤只是为了用户体验(如即时提示),永远不能替代服务端过滤,因为攻击者可以绕过浏览器直接发送请求。
- 输出转义,而非输入消毒:某些场景下(如富文本),保留特殊字符是必要的,更好的做法是存储原始数据,在输出时根据上下文转义(如展示时进行HTML转义)。
核心技术步骤
一个完整的过滤流程包括四个步骤:
- 验证:检查输入是否符合预期格式。
- 净化:移除或替换危险内容。
- 转义:将特殊字符转换为无害的表示形式(用于输出场景)。
- 编码:确保数据在不同上下文(如URL、JSON)中安全。
针对不同场景的过滤方法
防止 SQL 注入(最常见威胁)
- 推荐做法:参数化查询(Prepared Statements),永远不要拼接SQL字符串。
- 示例(Node.js + PostgreSQL):
SELECT * FROM users WHERE id = $1,通过参数传递userInput。
- 示例(Node.js + PostgreSQL):
- 替代做法:如果无法使用参数化查询(如存储过程),必须使用数据库驱动的转义函数(如
mysql_real_escape_string)。 - 不推荐:仅靠过滤单引号或关键字,容易被编码或注入避开。
防止 XSS(跨站脚本攻击,针对Web页面)
- 输出转义(核心):根据输出位置选择不同的转义方式。
- HTML 标签内:转义
<,>, , ,&。<script>。 - JavaScript 字符串内:使用
JSON.stringify或转义 、、。 - URL 参数内:使用
encodeURIComponent。
- HTML 标签内:转义
- 输入过滤(辅助):对于允许HTML的富文本输入(如评论),使用经过验证的白名单库(如
DOMPurify、bleach或sanitize-html),只允许特定的标签和属性(如<b>,<i>,<a href>)。
防止 命令注入(针对系统调用)
- 绝对禁止:永远不要将原始用户输入直接拼接到系统命令中。
- 推荐做法:
- 使用白名单限制可执行的命令名称或参数选项。
- 如果必须调用,使用参数化方式(如
execFile或subprocess.run传递数组),而不是字符串。 - 使用
shell=True时极度危险,应避免。
防止 文件路径遍历
- 严格验证:检查输入是否包含 、 或绝对路径(如
/etc/passwd)。 - 推荐做法:
- 将用户输入与一个安全的基础路径拼接前,先用
path.normalize()解析,然后检查解析后的路径是否以基础路径开头。 - 使用数据库或预定义的ID来映射文件,而非允许用户直接输入文件名。
- 将用户输入与一个安全的基础路径拼接前,先用
防止 逻辑或业务注入
- 类型与格式校验:
- 数值:使用
is_numeric()或Number.isNaN()。 - 邮箱/电话:使用正则验证格式,但不要过度依赖正则的完美性(RFC合规非常复杂)。
- 枚举值(如性别、国家):只接受预先定义好的白名单值(如
['male', 'female'])。
- 数值:使用
- 限制:
maxLength限制输入长度,防止缓冲区溢出或大数据存储问题。- 过滤不可见字符、控制字符(如
\x00)。
实用的通用过滤步骤(伪代码逻辑)
def sanitize_user_input(user_input, input_type):
# 1. 类型验证
if input_type == "integer":
if not user_input.isdigit():
return None, "必须是整数"
return int(user_input), None
if input_type == "email":
# 使用正则验证格式,但需注意复杂的RFC合规性
if not re.match(r"[^@]+@[^@]+\.[^@]+", user_input):
return None, "邮箱格式错误"
# 净化(移除可能导致注入的字符,如换行)
sanitized = re.sub(r'[\r\n]', '', user_input)
return sanitized, None
if input_type == "text":
# 通常用于评论/备注,需防止XSS
# 方案A:如果不需要HTML -> HTML转义
sanitized = html.escape(user_input)
# 方案B:如果需要简单HTML -> 使用白名单过滤库
# sanitized = bleach.clean(user_input, tags=['b', 'i', 'a'], attributes={'a': ['href']})
# 移除控制字符
sanitized = re.sub(r'[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]', '', sanitized)
return sanitized, None
# 默认处理:拒绝含有HTML标签或危险字符串的输入
if "<" in user_input or ">" in user_input or "'" in user_input:
return None, "输入包含不允许的字符"
return user_input, None
必须避免的错误做法
- 只在客户端过滤:用户可通过代理工具绕过前端验证。
- 依赖黑名单:如过滤
<script>标签,攻击者可以使用<SCRIPT>(大小写)、<img src=x onerror=>或编码方式绕过。 - 在过滤前就处理输入:先将用户输入拼接到SQL或HTML中,然后再尝试过滤,这已经为时已晚。
- 盲目使用
eval()或exec():任何动态执行用户输入的方式都是高危操作。
总结与最佳实践清单
| 步骤 | 操作 | 备注 |
|---|---|---|
| 定义规则 | 每个字段都要有明确的类型、长度、格式限制。 | 如 age 必须是0-150的整数。 |
| 输入时 | 使用白名单(允许字符集)和正则验证。 | 拒绝包含 <、 等的文本字段。 |
| 存储时 | 使用参数化查询操作数据库。 | 确保SQL注入不可能。 |
| 输出时 | 根据上下文(HTML、JS、URL)进行转义。 | 使用成熟的库(如 DOMPurify)。 |
| 架构上 | 分层防御,服务端必须有独立的过滤。 | 假设所有外部数据都是危险的。 |
核心建议:不要试图去“修复”用户输入中的恶意部分,而是确保它在你使用的每个环境(数据库、浏览器、Shell)中都是安全的。 使用成熟的、经过社区验证的库(如 brasp/validator、owasp/encoder、htmlpurifier)通常比自己手写正则更安全、更完善。