最有效防护是参数化查询,辅以白名单校验动态元素;输入校验仅作辅助筛除异常,无法替代参数化,因攻击者可通过大小写、编码、注释等绕过字符串匹配。

输入校验拦截关键字根本拦不住真实攻击
因为攻击者根本不按你预设的“关键词形态”出招。你写的 UNION SELECT 规则,对 UnIoN/**/SeLeCt、%55NION%20SEL%45CT、SELSELECTECT 完全无效——这些不是边缘案例,是渗透测试里天天跑通的绕过手法。
常见绕过方式包括:
- 大小写混写:
SeLeCt、uNiOn逃过小写正则 - 注释分割:
UNION/**/SELECT或/*!UNION*/SELECT,MySQL 解析时仍执行,但字符串匹配失败 - URL 编码:
%55NION(U 被编码)、%27(单引号),后端解码后才还原,但过滤器常在解码前或后错位执行 - 空格替换:
UNION%09SELECT(Tab)、UNION+SELECT(+ 号)、UNION%20SELECT(%20) - 双写嵌套:
UNIunionON→ 过滤器删掉中间union后剩UNION
输入校验只能处理“已知格式”的输入,不是 SQL 解析器
REGEXP、String.contains("select")、甚至 WAF 的规则引擎,都只是在做字符串扫描,不理解 SQL 语法结构。它无法识别:
- 括号嵌套:
(SELECT (SELECT 1))—— 正则很难递归匹配 - 注释包裹的恶意内容:
/*' OR 1=1 */ SELECT * FROM users - Unicode 同形字:
SELECT(全角字符)或\uFF23\uFF2F\uFF2D\uFF2D\uFF25\uFF2E\uFF34 - 数据库方言差异:PostgreSQL 支持
$$...$$字符串定界符,MySQL 不支持;SQL Server 用--和/* */,但解析行为不同
更关键的是:输入校验发生在请求进入应用之后,而 SQL 注入可能在拼接阶段就完成破坏——比如 f"WHERE id = {user_id}" 这种写法,校验再严也救不回来。
真正该拦截的位置不在输入层,而在 SQL 构建层
防御重心必须从“这段文本像不像恶意 SQL”转向“它会不会被当作代码执行”。这意味着:
- 所有用户输入参与 SQL 构建时,必须走参数化查询:
cursor.execute("SELECT * FROM users WHERE name = %s", [name])(Python/psycopg2)或conn.prepareStatement("SELECT * FROM users WHERE id = ?")(Java/JDBC) - 动态表名/字段名等无法参数化的场景,必须白名单硬校验:
if order_by not in ["created_at", "updated_at"]: raise ValueError - 禁用任何字符串拼接:
"WHERE id = " + str(user_id)、f"ORDER BY {field}"、MyBatis ${field}全部高危 - ORM 原生查询安全,但一旦调用
.raw()、.query()、text(...)且未绑定参数,就退回裸奔状态
输入校验唯一靠谱的用途:提前拒绝明显异常输入
它不是防线,而是“漏斗口”——用来快速筛掉明显非法数据,降低后续处理负载和日志噪音。实操建议:
- 数字型字段直接转
int或float,转换失败立即400拒绝 - 邮箱、手机号等用白名单正则:
^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$,不依赖黑名单 - 搜索词限制长度(如 ≤ 100 字符)、禁止控制字符(
\x00-\x08\x0B\x0C\x0E-\x1F\x7F)、过滤零宽字符(\u200B-\u200F) - 前端校验纯属体验优化,后端必须重复执行——
disabled属性、maxlength、React 的onChange校验,全可被 curl 绕过
最易被忽略的点:动态结构(如多租户 schema 切换、报表字段排序)既不能参数化,又不能靠正则兜底,必须结合白名单 + 最小权限 DB 账号 + 查询代理层(如 ProxySQL)做纵深防御。











