能#{}就#{},必须用${}时须严格白名单校验字段名、表名等动态sql片段,禁用未过滤的前端直传;druid监控页需关闭或强密码保护;jdbc原生调用必须用preparedstatement;接口签名需归一化参数并排除sql关键字字段。

MyBatis动态SQL里${}和#{}混用导致注入
直接拼接字段名、表名或排序字段时,用${}是唯一选择,但也是最危险的入口。比如ORDER BY ${sortField}或SELECT * FROM ${tableName},一旦sortField来自前端未校验参数,攻击者就能注入id; DROP TABLE users--这类语句。
真正安全的做法不是禁用${},而是加白名单校验:
- 所有被
${}引用的变量,必须先在Java层做严格匹配,例如:if (!Arrays.asList("id", "name", "create_time").contains(sortField)) throw new IllegalArgumentException(); - 避免从
request.getParameter()或@RequestParam直传到${},统一走DTO+校验注解(如@Pattern(regexp = "^[a-zA-Z_][a-zA-Z0-9_]*$")) - MyBatis-Plus的
QueryWrapper对orderBy等方法也默认不防注入,同样要手动校验字段名
Druid监控页暴露SQL语句与执行能力
Druid的/druid/sql.html页面如果没设密码,等于把所有执行过的SQL、参数、甚至执行框全开放给攻击者。尤其当SQL里含WHERE name = '${name}'这类写法时,攻击者能直接复现并篡改语句。
修复只需两步,但常被跳过:
- 关闭生产环境的Druid监控Servlet:
spring.datasource.druid.stat-view-servlet.enabled=false - 若必须启用,强制设置登录凭证:
spring.datasource.druid.stat-view-servlet.login-username=admin+spring.datasource.druid.stat-view-servlet.login-password=StrongPass123!,且密码不能是admin/admin123这类弱口令
非MyBatis场景:JDBC原生调用漏掉预编译
有些老代码或工具类仍用Statement拼接SQL,比如"SELECT * FROM user WHERE id = " + userId。这种写法在任何框架下都无条件触发SQL注入,连WAF都难拦截(因为参数在URL或Body里不显眼)。
PigX UI Pro 前端开发指南 - Vue 3 + TypeScript + Element Plus。当用户提到 PigX UI、PigX 前端、lgb-mgui 项目、Vue 3 企业级后台开发、Element Plus 后台开发时使用此技能。
必须全部替换为PreparedStatement:
- 查ID时用
SELECT * FROM user WHERE id = ?,再调ps.setInt(1, userId) - 批量IN查询不能写
WHERE id IN (${ids}),而要用WHERE id IN (?, ?, ?)动态生成占位符,再逐个set - Spring JDBC的
JdbcTemplate支持in参数自动展开,但前提是传入List而非拼接字符串
接口签名验证绕过导致参数污染
电商项目常见“token+sign”方案,但很多实现只验签名合法性,不验参数本身是否被篡改。攻击者可截获合法请求,把{"userId": "123"}改成{"userId": "123 OR 1=1"},只要sign重算正确,后端就照单解析。
关键点在于:签名前必须对原始参数做归一化处理,并排除高危字段:
- 签名计算前,先用
JSON.parse()再JSON.stringify()标准化参数顺序,防止键名大小写/空格干扰 - 禁止对
order、group、having等SQL关键字字段签名,这些字段应单独白名单校验 - 后端收到请求后,先验sign,再对每个字符串参数做
Pattern.compile("[^\w\s]").matcher(value).find()检查非法字符
真实漏洞往往藏在“看起来没问题”的组合里:MyBatis的${} + Druid监控页 + 未校验的接口签名,三者叠加才让一次普通查询变成数据库沦陷入口。别只盯着单点修复,得看清数据从请求头进来,经签名验证、参数解析、SQL构造,最后落到JDBC执行的整条链路里,哪一环松了扣,风险就从哪冒出来。










