很多项目写了“SQL 注入防护测试”,结果测到的只是 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 接口,测试请求体里应包含
%27这类 URL 编码后的单引号,并确认解码后仍然会被拦截
断言目标应是业务异常,而不是“不报错”
很多人看到测试没报错,就以为防护生效了,这个判断并不成立。数据库驱动对非法输入的反应并不一致:MySQL 可能直接抛出 MySQLSyntaxErrorException,PostgreSQL 也可能静默截断或返回空结果。只看“有没有异常”,无法证明防护发生在数据库执行之前。

更可靠的断言方式,是明确要求抛出业务层异常,比如 IllegalArgumentException 或自定义的 InvalidInputException。这样才能说明拦截逻辑在应用层已经生效。
- 正确示例:
assertThatThrownBy(() -> service.login("root' OR '1'='1"))配合.isInstanceOf(IllegalArgumentException.class) - 错误示例:
assertThatCode(() -> service.login("...")).doesNotThrowAnyException(),这只能说明程序没崩,不能说明请求没有进 DB - 如果实际抛出的是
PSQLException或SQLServerException,通常意味着防护失效,请求已经抵达数据库
高危分支要单独覆盖:${}、原生 SQL 与字符串拼接
真正危险的地方,往往不是常规参数绑定,而是那些绕开绑定机制的分支。MyBatis 的 ${}、JPA 的 createNativeQuery、Java 里的 String.format 和 + 拼接,都属于测试里必须显式覆盖的高风险写法。

尤其在旧项目中,拼接逻辑可能藏得很深。比如先从数据库取出模板 SQL,再把用户输入填入字段名或表名,这类场景即便表面上用了 #{},也不能说明整体链路是安全的。
- 对于使用
${tableName}的 mapper,可传入"users; DROP TABLE admins--",验证是否抛出预期异常 - 对于
String sql = "SELECT * FROM " + userInput;这类代码,测试应触发运行时异常,例如SQLException,或者至少留下非法输入审计日志 - 在 Hibernate 场景中,应检查是否使用了
@Param绑定,而不是createNativeQuery("SELECT * FROM users WHERE name = '" + input + "'")
不要把 H2 或 @Sql 当成防护测试本身
用内存数据库执行一条带 ' OR 1=1 的查询,再根据是否返回全表数据来判断安全性,这种思路看上去直观,实际上很容易误导。你测到的常常是 H2、hsqldb 或其他方言实现的 SQL 解析行为,而不是你在 Java 层写下的防护逻辑。
真正的防护通常发生在应用层,包括正则过滤、白名单校验和参数绑定调用。数据库只是最终执行者,不同数据库对非法输入的响应差异很大,把判断依据建立在 DB 是否报错上,结论往往不稳定。
- 不建议依赖
@Sql(scripts = "malicious-data.sql")这类测试方式,它无法证明你的校验逻辑是否真的触发 - 不要把 H2 或 hsqldb 的“报错即拦截”当成安全结论,因为它们并不模拟 MySQL 的注释解析,也不等同于 PostgreSQL 的类型强转行为
- 更值得检查的是审计日志里是否记录了
"Blocked SQL injection attempt: ..."这类拦截信息
别漏掉最隐蔽的二次注入路径
很多测试只盯着“当前请求是否立即触发注入”,却忽略了二次注入。所谓二次注入,是指恶意内容先被保存,再在后续查询、拼接或动态 SQL 生成时触发风险。这类问题在单元测试里最容易被漏掉。
典型场景是:注册时写入 "admin'; --",系统当时并没有执行危险 SQL;但登录、检索或后台管理查询时,又把这个值拼进了 SQL 模板,于是漏洞在第二次使用时才暴露。
因此,测试设计不能只覆盖即时接口,还要补上“先存后查”的完整链路,并同时检查异常与日志记录是否符合预期。













