preparedstatement不能替代所有statement,因表名、列名等标识符不可参数化;需分层处理:值用参数化,结构用白名单校验;like查询须预处理通配符并指定escape字符。

直接替换 Statement 为 PreparedStatement 不总是可行
很多老系统里,SQL 是用 StringBuffer 或 StringBuilder 拼出来的,甚至嵌套在 if/else 中动态拼接 WHERE 条件。这时候硬性要求“全部改用 PreparedStatement”会失败——因为表名、列名、ORDER BY 字段、GROUP BY 表达式这些**标识符(identifier)根本不能参数化**。盲目替换成 ? 会导致语法错误,比如 SELECT ? FROM ? 在任何数据库都执行不了。
真正要做的,是分层识别:哪些部分能安全参数化(值),哪些必须白名单校验(结构)。否则重构后要么报错,要么引入新漏洞。
先定位所有高危拼接点,别靠 grep 全文搜 "+"
单纯搜索字符串拼接符号会漏掉大量伪装写法,比如:
sql.append("WHERE id = ").append(id)String sql = String.format("UPDATE %s SET status=1", table)- MyBatis XML 中的
${tableName}或${orderBy}
推荐做法是结合工具扫描 + 人工聚焦:
- 用 SonarQube 或 Semgrep 配置规则,匹配
Statement.execute*、Connection.createStatement()、jdbcUrl后紧跟字符串拼接的上下文 - 重点检查 DAO 层基类、通用查询工具类、导出/报表/定时任务脚本中反复出现的 SQL 构建逻辑
- 对每个疑似点,确认输入来源:是硬编码?配置项?还是用户请求参数?只有后者才真正构成注入风险
对不可参数化的部分,强制走白名单 + 运行时断言
像 getTable()、getOrderByClause() 这类方法,返回值必须来自可控集合,不能靠“看起来安全”来赌。实际落地要三步闭环:
当用户要求为 Java 代码添加注释、修改注释、审查注释、整理注释格式,或要求按照代码注释规范处理 Java 文件时,使用本技能。本技能用于统一生成和维护 Java 类注释模板、按 Git 对比 release 判断版本号、补充每行代码上方中文块注释、调整注释空行规则,并保持 Java 文件 UTF-8 无 BOM 编码。典型触发场景包括:补充类注释、更新 @version、检查 @author 来源、整理行尾注释、把注释放到代码上一行、检查注释是否符合规范、构建前检查 Java 注释风格。
- 定义枚举或静态 Map,例如:
public enum ValidTable { USER, ORDER, PRODUCT } - 在方法内部做显式校验:
if (!ValidTable.contains(input)) throw new IllegalStateException("Invalid table name: " + input); - 单元测试覆盖非法输入,确保断言生效;CI 流程中加入 checkstyle 规则,禁止新增裸字符串拼接标识符的代码
注意:白名单必须是**穷举且不可绕过**的。用正则校验表名(如 ^[a-z_]{2,30}$)不如枚举可靠——攻击者可能利用合法命名撞库,比如构造表名 user_backup_2023 绕过简单规则。
LIKE 查询里的通配符和转义字符最容易被忽略
老系统里常见这种写法:"WHERE name LIKE '%" + userInput + "%'"。即使你把它改成 ps.setString(1, "%" + userInput + "%"),依然危险——因为用户输入里的 %、_ 会被数据库当作通配符执行,不是字面量。
正确做法是显式声明 ESCAPE 字符并统一处理:
- MySQL/PostgreSQL/Oracle 都支持
ESCAPE '$'语法,选一个不常出现在业务数据中的字符(如$或!) - 对用户输入预处理:
userInput.replace("$", "$$").replace("%", "$%").replace("_", "$_") - SQL 写成:
WHERE name LIKE ? ESCAPE '$',再把处理后的字符串设为参数
这个细节在批量重构时极容易遗漏,而且不同数据库对默认 ESCAPE 字符的处理不一致,上线前必须在目标 DB 环境实测验证。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南










