preparedstatement是唯一可靠起点,因mysql只解析完整sql字符串,字符串拼接使用户输入成为语法一部分,而preparedstatement在预编译阶段固定语法树,参数仅作纯数据绑定,单引号、分号等均不触发重解析。

直接用参数化查询,其他所有方案都是补丁或兜底手段。
为什么PreparedStatement是唯一可靠起点
MySQL本身不识别“用户输入”这个概念,它只解析传入的完整SQL字符串。一旦你用字符串拼接把username变量塞进WHERE username = 'xxx'里,MySQL就默认xxx是语法的一部分——哪怕它里面藏着' OR 1=1 --。只有PreparedStatement能让MySQL在解析阶段就固定语句结构,后续只替换占位符值,值里的单引号、分号、注释符全被当作纯字符串处理,不会触发词法重解析。
- Java中必须用
PreparedStatement,不能只靠Statement加StringEscapeUtils.escapeSql这类转义 - PHP中必须用
mysqli::prepare()或PDO的prepare(),mysql_real_escape_string已废弃且宽字节场景下可被绕过 - Node.js用
mysql2库时,必须调用connection.execute()而非connection.query()拼接字符串
#{} 和${} 在MyBatis里不是可选项,而是开关
MyBatis的#{} 底层就是PreparedStatement,而${} 是原样字符串替换——它连单引号都不加,等于直接把用户输入扔进SQL模板里。常见踩坑点:
-
ORDER BY ${sortField}:如果sortField来自前端,攻击者传id, (SELECT password FROM users LIMIT 1)就能拖数据 -
SELECT * FROM ${tableName}:表名无法参数化,但必须用白名单校验,比如if (!List.of("user", "order").contains(tableName)) throw new IllegalArgumentException(); - 动态
WHERE条件里混用#{}和${},比如AND status = #{status} ${extraCondition},extraCondition一旦没做严格过滤就等于开后门
WAF和输入过滤只能拦住脚本小子,拦不住真实攻击者
WAF规则本质是正则匹配,攻击者用admin'/*comment*/OR/**/1=1、URL编码%27、双写seleselectct就能绕过。输入过滤同理:
- 黑名单过滤
unionselect;等关键词,但MySQL支持UNION/**/SELECT、大小写混合、内联注释绕过 - 只在前端JS校验,后端完全不校验,等于没防
- 用
addslashes()处理,但MySQL开启GBK编码时,%A1%AA这种宽字节能吃掉反斜杠,让单引号逃逸
最容易被忽略的三个执行入口
很多人以为防住WHERE条件就安全了,其实以下三处常被遗漏:
- 存储过程调用:
CALL sp_login(?, ?)必须用CallableStatement,不能拼接CALL sp_login('admin', '123') - 批量操作:
INSERT INTO user VALUES (?, ?), (?, ?)要确保每个?都绑定独立参数,别图省事用String.format拼成一长串 - 日志埋点SQL:
"INSERT INTO log (sql) VALUES ('" + originalSql + "')"这种写法,原始SQL里带注入payload就会二次触发
真正的防御水位不在某一行代码,而在整个数据流是否始终维持“结构与数据分离”——只要有一处松动,前面所有防护都归零。











