深度解析:Spring MVC代码审计实战
字数 1858 2025-12-04 12:16:54
Spring MVC代码审计实战教学文档
1. Spring MVC框架基础
1.1 Spring Controller注解详解
@Controller
- 作用:标识类为Spring MVC控制器,主要用于页面跳转
- 示例:
@Controller
public class GoodsController {
@Resource
private NewBeeMallGoodsService newBeeMallGoodsService;
@GetMapping({"/search", "/search.html"})
public String searchPage(@RequestParam Map<String, Object> params, HttpServletRequest request) {
if (StringUtils.isEmpty(params.get("page"))) {
params.put("page", 1);
}
// 业务逻辑
}
}
@RestController
- 作用:@Controller + @ResponseBody组合注解,用于API接口开发
- 示例:
@RestController
public class ApiUserController {
@GetMapping("/api/user/{id}")
public User getUser(@PathVariable Long id) {
return userService.findById(id);
}
}
@RepositoryRestController
- 作用:扩展Spring Data REST自动生成的REST接口
- 示例:
@RepositoryRestController
public class CustomProductController {
@GetMapping("/products/search/byName")
public ResponseEntity<?> searchByName(@RequestParam String name) {
// 自定义逻辑
}
}
1.2 Spring MVC请求映射注解
| 注解 | HTTP方法 | 说明 |
|---|---|---|
| @RequestMapping | 任意(可指定) | 通用映射注解 |
| @GetMapping | GET | @RequestMapping(method = GET)的快捷方式 |
| @PostMapping | POST | 提交数据(表单、JSON) |
| @PutMapping | PUT | 全量更新资源 |
| @DeleteMapping | DELETE | 删除资源 |
| @PatchMapping | PATCH | 部分更新资源 |
2. 代码审计方法论
2.1 审计思路
- 识别开发框架:确认项目基于Spring MVC框架
- 定位API接口:通过Controller类查找所有路由
- 参数跟踪:从接口接收参数开始,跟踪参数流转路径
- 漏洞识别:分析每个代码逻辑点的安全风险
- 权限验证:区分前后台路由,检查鉴权逻辑
2.2 关键关注点
- 前台路由:直接面向用户访问的接口
- 后台路由:通常位于admin目录下,需要重点检查鉴权
- 参数处理:重点关注用户输入的数据处理逻辑
3. 常见漏洞类型及审计方法
3.1 权限绕过漏洞
漏洞原理
使用request.getRequestURI()方法进行路径判断时,攻击者可通过特殊字符绕过权限检查。
漏洞代码示例
// 漏洞代码 - 使用getRequestURI()进行权限判断
if (url.startsWith("/admin") && session.getAttribute("loginUser") == null) {
// 要求登录
}
绕过方法
- 使用分号
;:/admin;/index - 使用正斜杠
/:/admin//index
修复方案
使用规范化路径进行比较:
String path = request.getRequestURI().replaceAll("/+", "/");
if (path.startsWith("/admin") && session.getAttribute("loginUser") == null) {
// 要求登录
}
3.2 任意文件上传漏洞
漏洞特征
- 未对文件类型进行校验
- 文件保存路径可控
- 后台路由但权限可绕过
漏洞代码示例
@PostMapping("/admin/upload")
public String uploadFile(@RequestParam("file") MultipartFile file) {
String originalFileName = file.getOriginalFilename(); // 获取原始文件名
String suffix = originalFileName.substring(originalFileName.lastIndexOf(".")); // 提取后缀
String newFileName = getNewFileName(suffix); // 生成新文件名
// 直接保存文件,未做类型检查
file.transferTo(new File(Constants.FILE_UPLOAD_DIC + newFileName));
return "文件访问路径";
}
审计要点
- 查找文件上传相关路由(如包含upload、file等关键词)
- 检查文件类型验证逻辑
- 验证权限控制是否完备
3.3 支付逻辑漏洞
漏洞原理
支付成功状态仅凭用户传入参数判断,未验证支付真实性。
漏洞代码示例
@GetMapping("/paySuccess")
public String paySuccess(@RequestParam String orderNo) {
// 直接根据订单号标记支付成功,未验证支付真实性
payService.paySuccess(orderNo);
return "支付成功";
}
服务层漏洞代码
public void paySuccess(String orderNo) {
Order order = orderMapper.selectByOrderNo(orderNo);
if (order.getOrderStatus() == OrderStatusEnum.ORDER_PRE_PAY.getOrderStatus()) {
// 直接标记为已支付,无验证逻辑
order.setOrderStatus(OrderStatusEnum.ORDER_PAID.getOrderStatus());
orderMapper.updateByPrimaryKey(order);
}
}
修复方案
- 验证支付渠道回调签名
- 查询第三方支付平台确认支付状态
- 记录完整的支付流水信息
3.4 越权漏洞
3.4.1 水平越权(更新个人信息)
漏洞代码
@PostMapping("/user/update")
public Result updateUserInfo(@RequestBody MallUser mallUser, HttpSession session) {
MallUser user = userService.getUserById(mallUser.getUserId()); // 根据传入ID查询用户
if (user != null) {
// 未验证当前登录用户是否有权修改此用户信息
user.setNickName(mallUser.getNickName());
user.setIntroduceSign(mallUser.getIntroduceSign());
userMapper.updateByPrimaryKeySelective(user);
}
return ResultGenerator.genSuccessResult();
}
修复方案
@PostMapping("/user/update")
public Result updateUserInfo(@RequestBody MallUser mallUser, HttpSession session) {
Long loginUserId = (Long) session.getAttribute("loginUserId");
if (!loginUserId.equals(mallUser.getUserId())) {
return ResultGenerator.genFailResult("无权限修改此用户信息");
}
// 安全更新逻辑
}
3.4.2 水平越权(删除订单)
漏洞代码
@DeleteMapping("/shop-cart/{newBeeMallShoppingCartItemId}")
public Result deleteItem(@PathVariable("newBeeMallShoppingCartItemId") Long itemId) {
// 直接根据ID删除,未验证该订单是否属于当前用户
Boolean result = newBeeMallShoppingCartService.deleteById(itemId);
return ResultGenerator.genSuccessResult();
}
修复方案
@DeleteMapping("/shop-cart/{newBeeMallShoppingCartItemId}")
public Result deleteItem(@PathVariable("newBeeMallShoppingCartItemId") Long itemId, HttpSession session) {
Long userId = (Long) session.getAttribute("loginUserId");
// 验证订单归属
if (!shoppingCartService.isItemBelongToUser(itemId, userId)) {
return ResultGenerator.genFailResult("无权限删除此订单");
}
Boolean result = newBeeMallShoppingCartService.deleteById(itemId);
return ResultGenerator.genSuccessResult();
}
3.5 SQL注入漏洞
3.5.1 MyBatis中#{}和${}的区别
| 占位符 | 安全性 | 处理方式 |
|---|---|---|
| #{} | 安全 | 预编译参数,防止SQL注入 |
| ${} | 危险 | 直接拼接SQL,存在注入风险 |
3.5.2 漏洞代码示例
漏洞Mapper XML
<!-- NewBeeMallGoodsMapper.xml -->
<select id="getNewBeeMallGoodsPage" parameterType="Map" resultMap="BaseResultMap">
SELECT * FROM tb_newbee_mall_goods
WHERE
<if test="goodsName != null and goodsName != ''">
goods_name = '${goodsName}' <!-- 直接拼接,存在SQL注入 -->
</if>
</select>
对应的Mapper接口
public interface NewBeeMallGoodsMapper {
List<NewBeeMallGoods> getNewBeeMallGoodsPage(Map<String, Object> params);
}
Controller层调用
@Controller
@RequestMapping("/admin")
public class GoodsController {
@GetMapping("/goods/list")
public String list(@RequestParam Map<String, Object> params) {
// 直接接收所有URL参数
PageQueryUtil pageUtil = new PageQueryUtil(params);
List<NewBeeMallGoods> goodsList = goodsService.getNewBeeMallGoodsPage(pageUtil);
return "admin/goods";
}
}
3.5.3 另一处SQL注入示例
搜索功能漏洞
<!-- 搜索功能的Mapper -->
<select id="findGoodsListBySearch" parameterType="Map" resultMap="BaseResultMap">
SELECT * FROM goods
WHERE
<if test="keyword != null and keyword != ''">
(goods_name LIKE '%${keyword}%' OR goods_intro LIKE '%${keyword}%')
</if>
</select>
前台搜索Controller
@GetMapping({"/search", "/search.html"})
public String searchPage(@RequestParam Map<String, Object> params, HttpServletRequest request) {
String keyword = (String) params.get("keyword");
if (StringUtils.isNotEmpty(keyword)) {
keyword = keyword.trim();
}
// 参数直接传递给Service层,存在SQL注入风险
PageQueryUtil pageUtil = new PageQueryUtil(params);
model.addAttribute("pageResult", goodsService.searchNewBeeMallGoods(pageUtil));
return "mall/search";
}
3.5.4 SQL注入修复方案
方案1:使用#{}预编译
<select id="getNewBeeMallGoodsPage" parameterType="Map" resultMap="BaseResultMap">
SELECT * FROM tb_newbee_mall_goods
WHERE
<if test="goodsName != null and goodsName != ''">
goods_name = #{goodsName} <!-- 使用预编译 -->
</if>
</select>
方案2:业务层过滤
public List<NewBeeMallGoods> getNewBeeMallGoodsPage(Map<String, Object> params) {
// 对参数进行安全过滤
String goodsName = (String) params.get("goodsName");
if (goodsName != null) {
goodsName = SqlFilter.filter(goodsName); // 自定义SQL过滤函数
params.put("goodsName", goodsName);
}
return goodsMapper.getNewBeeMallGoodsPage(params);
}
4. 审计工具和技巧
4.1 代码搜索关键词
${:查找MyBatis中危险的SQL拼接getRequestURI():查找权限绕过风险点upload/file:查找文件上传功能admin:定位后台管理功能pay/order:查找支付相关逻辑
4.2 依赖检查
检查pom.xml文件,识别使用的第三方组件版本,查找已知漏洞:
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.6</version> <!-- 检查版本安全性 -->
</dependency>
5. 总结
Spring MVC代码审计需要系统性地分析框架特性、路由映射、参数处理和权限控制。重点关注:
- 权限控制:前后台路由区分、会话管理、访问控制
- 输入验证:参数过滤、SQL注入防护、文件类型检查
- 业务逻辑:支付流程、订单处理、用户数据操作
- 框架配置:安全配置、组件版本、依赖管理
通过系统化的审计方法,可以有效发现和修复Spring MVC应用中的安全漏洞。