SQL注入高危信号是WHERE等子句中拼接用户输入,必须用参数化查询;动态列名/表名需白名单校验;WAF应拦截非法语法结构而非单关键词;MyBatis中${}需严格白名单控制;新规则须先观测再上线。

SQL注入测试时,WHERE子句里拼接用户输入就是高危信号
只要看到字符串拼接进WHERE、ORDER BY或UNION位置,不管用不用PreparedStatement,都得立刻停住——这已经不是“可能被注入”,而是“正在裸奔”。动态SQL里混入request.getParameter("id")或args[0]这类原始输入,等于把数据库的执行权直接交出去。
实操建议:
- 所有外部输入必须走参数化查询:Java 用
PreparedStatement,Python 用cursor.execute("SELECT * FROM t WHERE id = %s", [user_id]),Node.js 用pg.query("SELECT * FROM t WHERE id = $1", [id]) - 禁止用字符串格式化拼接 SQL,包括
f"WHERE name = '{name}'"、"WHERE id = " + str(id)、String.format("WHERE status = '%s'", status) - 若必须动态列名或表名(如多租户分表),只能白名单校验:
if table_name not in ["user_2024", "user_2025"]: raise ValueError("invalid table")
WAF规则写成sqlmap -u能绕过的,等于没写
很多团队配 WAF 规则只拦 UNION SELECT 或 1=1,但 sqlmap 默认就用 AND (SELECT ...)=0、时间盲注、报错注入绕过。这类规则在真实红队测试面前形同虚设。
实操建议:
- 优先拦截非法语法结构,比如匹配
SELECT.*?FROM.*?WHERE.*?[\)\(]中嵌套括号+关键词组合,而不是单个关键词 - 对
ORDER BY后数字/字段名做长度和字符集限制(只允许 a-z、0-9、下划线,且长度 ≤ 32) - 禁用常见编码绕过:WAF 必须解码后再检测,否则
%27%20OR%201%3D1就会漏掉 - 别依赖单条规则,组合触发更可靠:比如同时命中“
information_schema”+“CONCAT”+“HTTP 头含X-Forwarded-For”才告警
mybatis里${}比#{}危险得多,但有时真躲不开
#{}会转成预编译参数,安全;${}是纯字符串替换,等价于手动拼 SQL。很多人以为“我只在ORDER BY里用${},问题不大”,但攻击者只要控制排序字段,就能注入id ASC, (SELECT password FROM user LIMIT 1)。
实操建议:
- 所有
${}使用点必须加白名单校验:比如排序字段限定为["created_at", "status", "score"],不在列表内直接 400 - 避免在
${}中拼接用户可控值,哪怕只是${prefix}_table——前缀也得从配置中心读,不能来自请求 - MyBatis-Plus 的
QueryWrapper是安全的,但自定义 XML 里一旦出现${ew.customSqlSegment},就得查清楚customSqlSegment来源
动态监测规则上线后,False Positive比False Negative更伤生产环境
规则太激进,一拦就拦住正常搜索词:“O'Reilly”、“C++ developer”、“10.10.10.10”全被当注入干掉,客服电话马上被打爆。而漏掉一次真实攻击,往往要等日志审计才发现。
实操建议:
- 新规则先开“观测模式”:记录匹配但不拦截,跑 3 天看误报率,超过 5% 就得调
- 区分路径敏感度:登录接口、密码重置页必须零容忍;商品搜索页可放宽,加二次人机验证代替硬拦截
- 把
HTTP Referer、User-Agent、请求频率一起作为判断依据,单一参数异常不触发阻断
真正难的不是写规则,是让规则既压得住自动化扫描器,又不妨碍真实用户打字时手抖多按了个单引号。上线前拿 sqlmap 跑一遍只是起点,还得看它怎么跟你的业务字段共存。










