Java代码审计实战:从SQL注入到权限控制
1. 代码审计的本质与常见误区
刚入行那会儿,每次听到"代码审计"这个词,总觉得是安全专家才能干的高端活。直到自己完整走完几个项目的审计流程才发现,这其实就是程序员日常开发的延伸技能。代码审计的核心,无非是用开发者的视角去发现那些可能被恶意利用的编码漏洞。
最常见的误区是把代码审计等同于渗透测试。实际上,前者是静态分析(不运行代码),后者是动态测试。举个例子:审计时发现MyBatis中使用了${}拼接SQL,就能直接判定存在注入风险,而不需要真的构造攻击参数。
另一个误区是认为只有安全团队才需要关注审计。其实在Spring Boot项目的CRUD开发中,程序员每写一个Repository接口,本质上都在做微观层面的代码审计。区别只是专业审计会更系统化,会建立完整的漏洞检查清单。
2. Java项目审计的核心检查点
2.1 SQL注入的典型场景
在Java生态中,SQL注入主要出现在三个层面:
- 原生JDBC:最经典的拼接漏洞
// 高危写法 String sql = "SELECT * FROM users WHERE name='" + name + "'";- ORM框架:
- MyBatis中使用${}而非#{}(动态SQL场景尤其要注意)
- JPA的@Query注解拼接字符串
- 存储过程调用:CallableStatement参数未过滤
经验:用阿里规约插件扫描时,所有"PreparedStatement未使用参数化查询"的警告都必须当作P0问题处理
2.2 权限控制漏洞
Spring Security配置不当会导致:
- 鉴权注解缺失:@PreAuthorize漏写
- CSRF防护关闭:.csrf().disable()出现在生产配置
- 接口路径通配:/api/**这样的宽松匹配
最近审计的一个案例:开发者在Controller写了@Secured("ROLE_ADMIN"),但Spring Security配置里根本没启用注解驱动:
@Configuration @EnableWebSecurity public class SecurityConfig { // 缺失@EnableGlobalMethodSecurity }2.3 反序列化风险
Fastjson、Jackson这些库的漏洞常被忽视。关键检查项:
ObjectMapper mapper = new ObjectMapper(); // 高危配置 mapper.enableDefaultTyping();建议的防御配置:
mapper.activateDefaultTyping( LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY );3. 企业级审计实战流程
3.1 准备工作清单
- 环境搭建:
- JD-GUI/IDEA反编译插件(审计第三方jar)
- OWASP ZAP/SonarQube(基础扫描)
- Git版本对比工具(看历史修复记录)
- 审计范围确认:
graph TD A[全部代码] -->|大型项目| B(采样审计) A -->|关键系统| C(全量审计) B --> D(核心业务模块+公共组件) C --> E(包含测试代码)- 规则定制: 根据公司技术栈调整检查项,比如:
- 使用MyBatis的项目:重点检查XML映射文件
- 用Redis缓存的:检查序列化方式
3.2 人工审计四步法
- 入口定位:
- 从Controller层向下追踪
- 特别关注
@RequestMapping中的路径参数
@GetMapping("/user/{id}") public User getUser(@PathVariable String id) { // 未做正则校验的路径变量可能引发注入 }- 数据流分析: 画出入参的完整传递路径,重点检查:
- 多层DTO转换时的数据净化
- 跨微服务调用时的参数传递
上下文验证: 同一个SQL查询在不同业务场景下风险等级不同。例如查询日志和查询支付记录的安全要求完全不同。
修复方案设计: 不是所有漏洞都要大改架构。比如旧系统暂时无法重构时,可以用Hibernate Validator做应急防护:
@Pattern(regexp = "^[a-zA-Z0-9_]+$") private String username;4. 高频漏洞修复示范
4.1 MyBatis动态SQL改造
问题代码:
<select id="findUsers" resultType="User"> SELECT * FROM users WHERE name LIKE '%${name}%' </select>修复方案:
<select id="findUsers" resultType="User"> SELECT * FROM users WHERE name LIKE CONCAT('%',#{name},'%') </select>特殊情况处理:当必须使用${}时(如动态表名),必须增加白名单校验:
// 校验逻辑示例 private static final Set<String> ALLOWED_TABLES = Set.of("users","products"); public String filterTableName(String input) { if(!ALLOWED_TABLES.contains(input.toLowerCase())){ throw new IllegalArgumentException("Invalid table name"); } return input; }4.2 Spring Security加固
典型弱配置:
http.authorizeRequests() .antMatchers("/admin/**").authenticated() .anyRequest().permitAll();强化方案:
http.authorizeRequests() .antMatchers("/admin/**").hasRole("ADMIN") .antMatchers("/api/**").authenticated() .anyRequest().denyAll() // 默认拒绝所有 .and() .csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());5. 自动化审计工具链
5.1 开源工具组合
- 静态扫描:
- SpotBugs:检测基础编码缺陷
- PMD:自定义规则检查
- 阿里规约插件:中文团队友好
- 依赖检查:
- OWASP DependencyCheck
- Snyk CLI
- IAST工具:
- Contrast Community Edition
- Hdiv Detection
5.2 自建规则示例
用ArchUnit做架构约束测试:
@ArchTest static final ArchRule no_jdbc_direct_call = noClasses().should().dependOnClassesThat() .resideInAnyPackage("java.sql..");用GitHook做提交拦截:
#!/bin/sh # pre-commit hook mvn spotbugs:check if [ $? -ne 0 ]; then echo "静态检查未通过!" exit 1 fi6. 审计报告编写要点
风险分级模板: | 等级 | 标准 | 修复时限 | |------|------|----------| | 紧急 | 可直接导致数据泄露 | 24小时 | | 高危 | 需特定条件触发 | 1周 | | 中危 | 影响范围有限 | 1个月 |
修复建议写法: 错误示范:"建议修复SQL注入" 正确写法: "在UserMapper.xml第47行,将${searchKey}替换为#{searchKey},并在Service层添加长度校验:
if(searchKey.length() > 100){ throw new InvalidParameterException(); } ```" 3. **证据留存**: - 漏洞代码截图(含行号) - 请求样本(如有) - 调用栈信息 ## 7. 开发者自检清单 每天提交代码前快速检查: 1. [ ] 所有用户输入是否经过校验? 2. [ ] 数据库查询是否使用参数化? 3. [ ] 配置文件是否包含敏感信息? 4. [ ] 新接口是否添加权限注解? 5. [ ] 第三方库版本是否已扫描? 对于核心业务模块,建议额外检查: - 资金计算是否用BigDecimal - 批量操作是否加锁 - 日志是否记录关键操作 ## 8. 企业级实践建议 1. **流程嵌入**: - 代码评审模板中加入安全检查项 - CI流水线加入安全扫描阶段 - 上线前必须完成审计报告 2. **知识沉淀**: - 建立内部漏洞案例库 - 编写框架安全编码规范 - 定期做漏洞复盘会 3. **红蓝对抗**: - 每季度组织内部攻防演练 - 对暴露的问题做根本原因分析 - 重要系统安排专项审计 在金融项目实践中我们发现,80%的安全问题可以通过基础编码规范避免。最近推动团队在IDEA里配置了实时检测模板,新人提交SQL注入漏洞的几率直接下降了60%。这比事后审计效率高得多。