真正有效的测试必须让恶意输入穿透校验层、拼接逻辑和dao调用,触发业务层异常(如illegalargumentexception),而非仅验证不报错;需覆盖${}拼接、二次注入等高危路径,并检查审计日志。

测试必须传真实恶意字符串,不能 mock DAO 返回值
很多测试失败不是因为防护没写对,而是测试本身绕过了关键路径。比如在 service 层 mock 了 userRepository.findByUsername 的返回值,却没把 "admin'--" 真正送进方法体——这等于测了个假流程。
真正有效的测试要让输入穿透校验层、拼接逻辑(如果有)、DAO 调用,直到触发异常或安全执行。否则你测的不是“防护是否生效”,而是“mock 是否配对成功”。
- 用
userService.findByUsername("admin'--")调用,而不是when(userRepository.findByUsername(any())).thenReturn(...) - 如果底层是 MyBatis,确保测试走的是
select * from users where name = #{name}路径,而非${name}拼接分支 - 对 JSON 接口,测试 body 原始内容含
%27(URL 编码单引号),确认解码后仍被拦截
断言目标不是“不报错”,而是“抛指定业务异常”
看到测试通过就以为安全?错。很多框架默认允许单引号,仅靠数据库驱动语法校验不可靠——MySQL 可能直接报 MySQLSyntaxErrorException,PostgreSQL 却可能静默截断或返回空结果。
真正要验证的是:防护逻辑是否在 DB 执行前就拦截了。所以 assert 必须明确指向业务层异常,比如 IllegalArgumentException 或自定义的 InvalidInputException。
- ✅ 正确:
assertThatThrownBy(() -> service.login("root' OR '1'='1"))+.isInstanceOf(IllegalArgumentException.class) - ❌ 错误:
assertThatCode(() -> service.login("...")).doesNotThrowAnyException()—— 这只说明没崩,不代表没进 DB - 若实际抛出
PSQLException或SQLServerException,说明防护失效,请求已抵达数据库
覆盖 ${} 和字符串拼接等危险写法
MyBatis 的 ${}、JPA 的 createNativeQuery、Java 的 String.format 或 + 拼接,都是高危点。单元测试必须显式构造这些分支并验证其行为。
尤其注意旧代码里藏得深的拼接逻辑:比如先从 DB 查出模板 SQL,再用用户输入填充字段名或表名——这种场景 #{} 也救不了你。
- 对使用
${tableName}的 mapper,传入"users; DROP TABLE admins--",验证是否抛出预期异常 - 对
String sql = "SELECT * FROM " + userInput;这类代码,测试需触发运行时异常(如SQLException)或记录非法日志 - Hibernate 场景下,检查是否用了
@Param绑定,而非createNativeQuery("SELECT * FROM users WHERE name = '" + input + "'")
避免用 @Sql 或 H2 执行真实 SQL
用内存数据库跑一条带 ' OR 1=1 的查询,看它返回全表数据——这不是在测你的防护,是在测 H2 的 SQL 解析器是否够严。
真实防护逻辑发生在 Java 层:正则过滤、白名单校验、参数绑定调用。数据库只是最终执行者。把测试焦点放在 DB 层,等于把责任推给方言实现,而不同数据库对非法输入的响应差异极大。
- 禁用
@Sql(scripts = "malicious-data.sql")类测试,它无法验证你写的校验逻辑是否触发 - 不要依赖 H2/hsqldb 的“报错即拦截”行为——它们不模拟 MySQL 的注释解析、PostgreSQL 的类型强转
- 重点检查日志:当恶意输入进来时,是否记录了
"Blocked SQL injection attempt: ..."这类审计日志
最常被忽略的一点:二次注入。测试不能只盯着即时接口,还要覆盖“先存后查”路径——比如注册时存入 "admin'; --",登录时再查这个字段触发拼接。这种延迟执行的漏洞,单元测试里最容易漏掉。











