sql注入长期居owasp top10首位的根本原因是业务代码中仍大量使用字符串拼接(如+、string.format、mybatis ${}),而开发者误判预编译适用边界;preparedstatement在表名、列名、order by、in动态列表等场景失效,必须辅以白名单校验;orm框架若误用原生拼接或不安全语法同样中招;权限管控与ci/cd静态扫描缺位加剧风险。

SQL 注入至今仍是 OWASP Top 10 中最常被利用、修复率最低的漏洞类型,根本原因不是技术不可控,而是 拼接字符串写法在业务代码中依然大量存活,且开发者对 prepareStatement / parameterized query 的适用边界存在误判。
为什么 String.format 或 + 拼接 SQL 还在用
很多团队已禁用 Statement.execute,但没意识到:只要 SQL 字符串里混入了未过滤的用户输入(哪怕只有一处),就等于给攻击者开了后门。
-
String.format("SELECT * FROM user WHERE name = '%s'", request.getParameter("name"))—— 单引号闭合后可任意追加OR 1=1 -
"SELECT * FROM order WHERE status IN (" + statusListStr + ")"——statusListStr = "0,1,2) OR 1=1 --"直接绕过括号限制 - 连 MyBatis 的
${}语法也常被误用于动态表名/列名,而它不走预编译,等同于字符串拼接
PreparedStatement 不是万能解药,关键看怎么用
用对了能拦截 95% 的注入,但以下场景它完全失效:
- 表名、列名、排序字段(
ORDER BY ?会报错,数据库不支持参数化这些位置) - IN 子句中的元素个数不确定(
WHERE id IN (?, ?, ?)必须提前知道问号数量) - 调用存储过程时传入的
procedureName是变量(如call ?()不合法)
这些情况必须结合白名单校验或正则严格匹配:Pattern.compile("^[a-zA-Z_][a-zA-Z0-9_]*$").matcher(input).matches(),而不是做“模糊过滤”。
ORM 框架也不能自动免疫
像 Hibernate/JPA 的 @Query 注解如果用了 value = "SELECT * FROM user WHERE name = '" + name + "'",照样中招;MyBatis 的 #{} 安全,但 ${} 就是裸奔。
- JPA Criteria API 是安全的,但写起来冗长,容易被绕开改用原生 SQL
- Laravel Eloquent 的
whereRaw()、Django 的extra()或raw()同样需要人工保证内容可信 - 日志中打印完整 SQL 时若包含用户输入,还可能引发敏感信息泄露(二次风险)
CI/CD 流程里缺一道“SQL 字符串扫描”卡点
静态扫描工具(如 Semgrep、SonarQube 规则)可以识别出高危模式,但多数项目没启用或规则太宽松。建议在 pre-commit 或 PR 阶段强制检查:
- 禁止出现
Statement.、.execute(.*\+.*")、\$\{.*\}(MyBatis)、concat\(|\|\|(Oracle/PostgreSQL 字符串拼接) - 要求所有外部输入进入 SQL 前必须经过
Objects.requireNonNull+ 白名单验证,而非仅做trim()或replace("'", "") - DBA 应限制应用账号权限——禁用
LOAD_FILE、INTO OUTFILE、多语句执行(allowMultiQueries=false)
真正难的不是写出安全代码,而是让每个开发者在写 WHERE 条件时,下意识想一句:“这个值,我敢不敢把它放进白名单?”











