A. 系统概要
“球易(Easyball)”是一款体育社交 Web 应用平台系统。系统旨在打破运动爱好者的信息孤岛,提供便捷的赛事管理、俱乐部运营与竞技数据统计等核心服务。
A1 相关技术栈
| 技术领域 | 具体技术 | 说明 |
|---|---|---|
| 后端框架 | Spring Boot 2.x | 核心应用框架,提供自动配置、依赖注入等 |
| 安全框架 | Spring Security | 负责认证(Authentication)与授权(Authorization) |
| 持久层框架 | MyBatis-Plus | 简化 CRUD 操作,支持条件构造器、逻辑删除等 |
| 数据库 / 缓存 | MySQL 8.0 / Redis | 业务数据落地与无状态 JWT 登录会话缓存 |
| 序列化 / 令牌 | FastJSON / JJWT | Redis 缓存序列化(开启 autoTypeSupport);JWT 密钥硬编码 |
A2 代码结构核心视图
com.micer.easyball
├── EasyballApplication.java # 启动类
├── config # 跨域、Redis 及 Security 鉴权配置
├── controller # API 路由控制层 (Club/Game/File 等)
├── service # 业务逻辑层 (Service / Impl)
├── mapper # MyBatis-Plus 数据访问层
├── pojo # 领域模型 (Entity / Dto)
├── expression
│ └── MyExpressionRoot.java # 自定义SpEL权限表达式(@ex.hasRole)
├── filter
│ └── JwtAuthenticationTokenFilter.java # JWT核心拦截验签器
└── exception # 全局异常处理 (AccessDenied / EntryPoint)
A3 部署与运行
1. 部署环境要求
1.8+
8.0+ (utf8mb3)
5.0+
3.6+
2. 部署与启动流程
1数据库初始化
# 1. 登录 MySQL
mysql -u root -p
# 2. 创建数据库
CREATE DATABASE easyball CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci;
# 3. 执行建表脚本
USE easyball;
SOURCE /path/to/CreateTable.sql; // 注意替换为实际绝对路径
2配置文件修改
编辑 src/main/resources/application.yml,填入实际凭证:
spring:
datasource:
username: "你的数据库用户名" # 修改
password: "你的数据库密码" # 修改
url: jdbc:mysql://localhost:3306/easyball
driver-class-name: com.mysql.cj.jdbc.Driver
redis:
host: localhost # 如 Redis 不在本机,修改 IP
server:
port: 8095 # 可修改服务端口
3项目构建
# 进入项目根目录
cd easyball
# Maven 打包跳过测试
mvn clean package -DskipTests
# 生成的 jar 包位于 target/ 目录
4启动 Redis
redis-server.exe。启动后会弹出运行终端(黑框框),测试期间请保持运行,注意不要关闭。
5启动运行
# 方式一:直接运行编译后的 jar 包
java -jar target/easyball-0.0.1-SNAPSHOT.jar
# 方式二:开发环境直接运行主类(推荐方式)
# 在 IDEA 等编辑器中执行 main 方法
3. 服务验证卡点
| 检查项 | 验证方式 |
|---|---|
| 服务启动 | 控制台无 Error,成功输出 Started EasyballApplication |
| 端口监听 | 本地访问 http://localhost:8095 |
| 数据库连接 | 使用 Postman 调用登录接口 /user/login 能获取到正常 JSON 响应 |
| Redis 连接 | 登录成功后,利用 Redis 可视化工具查看到存在 Easyball Login User:* 键 |
B. 功能用例
B0 系统用户 (RBAC 模型)
👑 系统管理员
(ROLE_ADMIN)
平台最高权限。负责宏观管理,可新增底层用户,具备创建全新俱乐部的权限。
💼 俱乐部管事
(ROLE_CLUB_ADMIN)
拥有管辖俱乐部成员生命周期的权限,以及对本俱乐部参与赛事的增删改及照片管理权限。
🏃 普通球员
(ROLE_CLUB_PLAYER)
权限限制在“数据查询”与“资源只读”。可查看花名册、历史比赛、下载照片及浏览年度统计。
B1 核心业务用例表
| 业务模块 | 功能描述 | 鉴权角色限制 |
|---|---|---|
| 用户认证 | 手机号+密码登录、签发 JWT、退出登录 | 匿名 / 所有已登录用户 |
| 俱乐部管理 | 新建俱乐部实体、获取所属球员列表名单 | ADMIN (新建) / PLAYER (查看) |
| 成员管辖 | 将用户拉入俱乐部、踢出成员、变更管理权限 | CLUB_ADMIN (局部管辖) |
| 比赛调度 | 查询比赛、录入新场次、篡改信息、删除场次 | CLUB_ADMIN (写) / PLAYER (读) |
| 静态资源 | 上传比赛精彩瞬间、下载照片、删除照片 | CLUB_ADMIN (写) / PLAYER (读) |
B2 功能截图展示
C. 后端 API 接口资产
系统全量路由清单,徽章颜色标识 HTTP 动词属性。
1. 身份认证与用户配置
| POST | /user/login |
用户登录 (返回 Token) | 匿名 |
| PUT | /user/modifyRole |
修改俱乐部成员角色 | ROLE_CLUB_ADMIN |
| POST | /user/addUserToClub |
添加用户到当前俱乐部 | ROLE_CLUB_ADMIN |
2. 赛事与场次管理 (高危攻击面)
| GET | /game/getGames/{year} |
查询某年份所有比赛 | ROLE_CLUB_PLAYER |
| POST | /game/addGame |
新增比赛 (需防刷榜) | ROLE_CLUB_ADMIN |
| PUT | /game/editGame |
修改比赛 (防越权/批量赋值) | ROLE_CLUB_ADMIN |
| DELETE | /game/deleteGame |
删除单场比赛 | ROLE_CLUB_ADMIN |
3. 文件管控
| POST | /file/upload |
上传比赛照片 (Multipart) | ROLE_CLUB_ADMIN |
| GET | /file/download |
下载照片 (需防目录遍历) | ROLE_CLUB_PLAYER |
D. 数据库模型
核心表结构总览
- user 系统用户账号体系
- club / club_player 俱乐部实体与权限映射
- game 比赛核心数据 (比分/场次)
- game_player 个人表现 (进球/MVP)
- player_last_club 上下文鉴权基石
初始化测试资产
• 2 个系统管理员账号 (包含 BCrypt 加密密码)
• 2 个测试俱乐部 (湛江华工校友 / 顺德茂商)
关键 DDL 定义 (SQL)
CREATE TABLE `user` (
`id` char(19) NOT NULL,
`telephone` varchar(255) NOT NULL UNIQUE,
`password` varchar(255) NOT NULL COMMENT 'BCrypt密文',
`role_list` varchar(255) NOT NULL COMMENT '系统角色JSON',
`is_enable` boolean NOT NULL DEFAULT true,
PRIMARY KEY (`id`)
);
CREATE TABLE `game` (
`id` char(19) NOT NULL COMMENT '比赛ID',
`our_team_id` char(19) NOT NULL COMMENT '属主俱乐部',
`our_score` BIGINT NOT NULL DEFAULT 0,
`opponent_score` BIGINT NOT NULL DEFAULT 0,
`venue` VARCHAR(255) NOT NULL,
PRIMARY KEY (`id`)
);
E. OWASP API Security Top 10 (2023) 详解
本区域系统性梳理了 2023 版 API 安全十大风险,并提供原理分析、防御机制以及基于 Easyball 系统的实战案例。 💡 快捷导航:您可以直接点击下方对应的漏洞缩略图,快速平滑跳转至该 API 的详细解析区域。
API1:2023 失效的对象级别授权 (BOLA)
风险定义: 在旧版中常被称为"水平越权"。当 API 暴露了处理对象(如数据库记录)的端点,允许客户端通过提供对象 ID 来访问或修改该对象时,如果服务器没有校验当前登录用户是否有权限操作该特定 ID 的对象,就会产生 BOLA 漏洞。
⚠️ 危害与识别
- 攻击者可篡改/删除他人数据,破坏业务隔离性。
- 端点包含对象 ID(如
/editGame?id=xxx)。 - 服务端仅依赖客户端 ID 进行 DB 操作,未验证所有权。
🛡️ 防范机制
- 基于会话的校验: 优先从 JWT/Security Context 取出真实身份。
- 属主校验: 执行操作前,查出记录比对属主 ID。
- IDOR 防护: 避免使用自增 ID,改用随机 UUID。
// 核心修复逻辑示例
Game dbGame = gameMapper.selectById(gameDto.getId());
if (!dbGame.getOurTeamId().equals(getCurrentClubId())) {
throw new AccessDeniedException("您没有权限修改其他俱乐部的比赛数据!");
}
🔥 案例一:比赛信息越权修改
A. 风险分析
球易系统的 PUT /game/editGame 接口允许客户端传入完整的 GameDto 对象,其中包含 id 字段用于定位目标比赛记录。服务端在处理更新请求时,仅从 JSON 中提取 id 并直接执行数据库更新操作,未验证当前登录用户所属俱乐部与目标比赛记录的 our_team_id 是否匹配。
这种"过度信任客户端"的设计,使得任何持有合法 Token 的用户,只要知道(或猜测到)其他俱乐部的比赛 ID,即可篡改该比赛数据。即使 ID 使用 Snowflake 算法生成,仍可通过其他接口泄露或规律猜测获取。
B. 攻击实施
- 身份获取: 攻击者以普通用户"陈硕"(手机号 180...)身份登录系统,获取 Token。
- 目标识别: 攻击者通过接口查询或 URL 规律,获取到属于"顺德茂商"的比赛 ID(如
9999999999999999999)。 - 请求构造: 在 Postman 中构造
PUT /game/editGame,篡改id,并注入恶意数据"ourScore": 0, "opponentScore": 99。 - 关键证据: 发送请求后,服务端未返回
403 Forbidden,目标比赛数据被成功篡改(更新操作穿透到数据库层)。
C. 安全加固 (资源属主校验)
核心原则: 基于服务端会话进行资源属主校验,绝不信任客户端传来的对象 ID。
@Override
@Transactional
public ResponseResult editGame(GameDto gameDto) {
// 1. 获取当前登录用户 ID 及其所属俱乐部
String currentUserId = getCurrentUserId();
String currentClubId = getCurrentClubId(currentUserId);
// 2. 查询目标比赛记录
Game dbGame = gameMapper.selectById(gameDto.getId());
if (dbGame == null) return new ResponseResult(404, "比赛不存在");
// 3. 核心校验:比对当前用户俱乐部与比赛所属俱乐部
if (!dbGame.getOurTeamId().equals(currentClubId)) {
logSecurityEvent("BOLA_ATTEMPT", currentUserId, gameDto.getId()); // 记录安全审计日志
return new ResponseResult(403, "您没有权限修改其他俱乐部的比赛数据");
}
// 4. 权限校验通过后,仅允许修改白名单字段
dbGame.setVenue(gameDto.getVenue());
dbGame.setDate(gameDto.getDate());
gameMapper.updateById(dbGame);
return new ResponseResult(200, "修改成功");
}
D. 相关知识
- IDOR: BOLA 的前身概念,防御核心是从"基于 ID 的访问"转变为"基于属主的访问控制"。
- Snowflake ID 安全性: 生成规律可预测,不能作为访问控制手段。敏感资源应使用完全随机的 UUID v4。
- 防御纵深: 仅靠"ID 难以猜测"是"隐匿即安全"的误区,必须在服务端横向校验所有权。
🔥 案例二:文件下载接口缺失对象级归属校验
A. 风险分析
FileController.download() 方法仅进行了角色级校验(ROLE_CLUB_PLAYER),未校验请求的文件是否属于当前用户所在俱乐部的比赛。上传接口返回的路径包含 gameId。攻击者只要知道其他比赛的 gameId 和文件名,即可越权下载非本俱乐部的敏感照片。
B. 攻击实施
- 获取目标: 通过查询或目录遍历猜测,获取其他俱乐部的图片路径
/image/17609.../abc.jpg。 - 越权请求: 使用普通球员的 Token 发起
GET /file/download?fileName=...请求。 - 关键证据: 返回 200 及图片二进制数据,证明越权下载成功。
C. 安全加固 (FileAccessService 统一校验)
提取路径中的业务标识(gameId)并实施严格的属主映射比对:
@GetMapping("/file/download")
@PreAuthorize("@ex.hasRole('ROLE_CLUB_PLAYER')")
public void download(@RequestParam String fileName, HttpServletResponse response) throws IOException {
String userId = getCurrentUserId();
// 对象级权限校验:深度鉴定该文件是否属于该用户的俱乐部
if (!fileAccessService.canAccessFile(userId, fileName)) {
response.setStatus(403);
response.getWriter().write("无权访问该文件");
return;
}
// ... 后续文件读取逻辑
}
API2:2023 失效的认证 (Broken Authentication)
风险定义: API 的认证机制存在缺陷,导致攻击者可以绕过认证、冒充其他用户或获取敏感凭证。常见原因包括:弱密码策略、会话管理不当、JWT 实现缺陷等。
⚠️ 危害与识别
- 密码策略薄弱(如默认密码未改,加密强度不够)。
- JWT 密钥硬编码、可预测。缺乏登录失败锁定。
- 错误信息过于详细(泄露账号存在与否)。
🛡️ 防范机制
- 强密码策略与 MFA: 强制复杂度,敏感操作二次验证。
- JWT 安全: 随机生成强密钥,设置短过期时间。
- 防爆破锁定: 连续失败 5 次后锁定 15 分钟。
🔥 案例一:登录失败提示差异化导致用户名枚举
A. 风险分析
UserDetailsServiceImpl.java 中,用户名不存在时返回 "用户名错误",密码错误时由 Spring Security 上层返回不同提示。攻击者可通过批量尝试手机号,根据返回错误信息的差异判断该手机号是否已注册系统。一旦确认账号存在,即可针对性地开展暴力破解、密码喷洒或社会工程学攻击。
B. 攻击实施
- 准备字典: 攻击者准备一批手机号列表(如 13800138000 到 13800138999)。
- 批量发包: 对每个手机号调用
POST /user/login,密码随意填写。 - 差异化观察: 若返回 "用户名错误" → 该手机号未注册;若返回 "登录失败" 或密码错误提示 → 该手机号已注册。
- 攻击效果: 收集所有"已注册"手机号,缩小暴力破解范围。
C. 安全加固 (统一错误提示)
统一错误提示,不暴露账号是否存在。并在全局异常处理器中兜底:
@Override
public UserDetails loadUserByUsername(String telephone) throws UsernameNotFoundException {
User user = userMapper.selectOne(new QueryWrapper<User>().eq("telephone", telephone));
// 统一错误提示,不暴露账号是否存在
if (Objects.isNull(user)) {
throw new BadCredentialsException("用户名或密码错误"); // 使用 Spring Security 标准异常
}
List<String> list = new ArrayList<>();
list.add(user.getCurrentRole());
return new LoginUser(user, list);
}
// 全局认证异常处理器
@ExceptionHandler(AuthenticationException.class)
@ResponseBody
public ResponseResult handleAuthenticationException(AuthenticationException e) {
return new ResponseResult(401, "用户名或密码错误");
}
D. 相关知识
- 用户名枚举: 认证系统的信息泄露漏洞,是暴力破解、密码喷洒(Password Spraying)、凭证填充攻击的前置步骤。
- 安全错误提示原则: 认证失败时,应返回模糊且统一的错误信息(如"用户名或密码错误"),绝不向客户端暴露差异化原因。
🔥 案例二:默认密码为手机号导致弱密码策略
A. 风险分析
UserServiceImpl.addUser() 方法在创建新用户时,将用户密码直接设置为手机号。虽然密码经过 BCrypt 哈希存储,但明文密码空间极度缩小(仅 10^11 种组合),且手机号在平台中易于获取。更危险的是,预置管理员账户使用了 123456 等常见弱密码,极易被爆破。
B. 攻击实施
攻击者通过
GET /club/getPlayers 获取所有成员手机号。选择目标手机号作为账号和密码调用 /user/login,成功率极高。
攻击者针对管理员账号进行常见弱口令尝试(如 123456),成功登入并获取高权限 Token。
C. 安全加固 (强随机与强制改密)
// 步骤一:生成 12 位包含大小写、数字、特殊字符的强随机密码
// SecureRandom random = new SecureRandom(); ...
// 步骤二:强制首次登录修改密码 (UserServiceImpl)
user.setPassword(passwordEncoder.encode(rawPassword));
user.setForceChangePassword(true); // 数据库新增字段标记需修改密码
// 步骤三:登录时检查强制修改密码 (LoginServiceImpl)
if (loginUser.getUser().getForceChangePassword()) {
return new ResponseResult(402, "请修改初始密码", Map.of("requireChange", true));
}
D. 相关知识
- 密码熵 (Password Entropy): 11位纯数字密码熵约 36.5 bit,现代 GPU 秒级破解;12位混合字符密码熵约 74 bit,无法被暴力破解。
- SecureRandom: 生成密码必须使用
java.security.SecureRandom,不可使用种子可预测的java.util.Random。
🔥 案例三:JWT 注销后 Token 仍然有效
A. 风险分析
系统的登出逻辑仅删除了 Redis 中的用户信息。但 JwtAuthenticationTokenFilter 如果未从 Redis 中找到信息,仅抛出"未登录"异常;如果 Token 被黑客截获,服务端无法主动"作废"一个已签发且签名合法的 Token。JWT 的无状态特性导致登出操作成为虚设。
B. 攻击实施
- 用户 A 正常登录获取 Token。业务操作期间 Token 被网络嗅探泄露。
- 用户 A 主动请求
POST /user/logout注销成功。 - 攻击者使用截获的旧 Token 访问
GET /user/getCurrentRole,接口依然返回 200 成功,旧 Token 依旧有效!
C. 安全加固 (Redis Token 黑名单机制)
在过滤器中增加对黑名单的拦截,并在注销时将 Token 剩余有效期写入 Redis 黑名单:
// 1. 登出时加入黑名单 (LoginServiceImpl)
long remainingTime = (expirationTime - System.currentTimeMillis()) / 1000;
if (remainingTime > 0) {
redisUtil.set("token_blacklist:" + token, "1", remainingTime);
}
// 2. 过滤器增加黑名单校验 (JwtAuthenticationTokenFilter)
if (redisUtil.hasKey("token_blacklist:" + token)) {
sendUnauthorized(response, "Token已失效");
return;
}
D. 相关知识
- JWT 的无状态困境: 享受了无状态易扩展的好处,就丧失了主动撤销的能力。通常需结合 Redis 黑名单或短期 Token + Refresh Token 机制解决。
- 黑名单机制: 仅存储失效 Token 并在过期时自动剔除,内存占用小,是解决 JWT 注销问题的最佳实践。
API3:2023 失效的对象属性级授权 (BOPLA)
风险定义: 表现为"过度数据暴露"或"批量赋值"。当系统未对客户端传入的数据进行严格的字段过滤,允许用户覆盖本不应由其控制的敏感对象属性(如比分、金额、权限)时,即存在此漏洞。
⚠️ 危害与识别
- API 接收完整 Entity 对象而非 DTO 进行更新。
- 利用框架自动绑定机制(@RequestBody)时未加限制。
- 响应体中暴露出不必要的内部 ID 或密码 Hash。
🛡️ 防范机制
- DTO 白名单: 定义仅包含允许修改字段的 UpdateDto。
- 响应过滤: 使用
@JsonIgnore隐藏核心字段。
// 修复前:存在批量赋值漏洞,接收完整对象直接 updateById
// 修复后:严格控制允许更新的字段
Game oldGame = gameMapper.selectById(dto.getId());
oldGame.setVenue(dto.getVenue()); // 仅更新白名单字段
// oldGame.setOurScore(...); // 绝不从客户端获取比分
gameMapper.updateById(oldGame);
🔥 案例一:比赛属性批量赋值篡改
A. 风险分析
球易系统的 PUT /game/editGame 接口接收完整的 GameDto 对象,服务端通过 Game game = gameDto; 直接将 DTO 转换为实体类,并调用 updateById 执行全字段更新。这种设计缺乏字段白名单机制,客户端传入的 JSON 中任何字段都会被同步到数据库。
攻击者利用此缺陷,可以在请求体中注入本不应由其控制的敏感属性(如我方得分 ourScore、对方得分 opponentScore)。虽然本案例与 API1 都针对 editGame 接口,但侧重点不同:API1 侧重“操作了不该操作的对象”,本案例侧重“修改了不该修改的属性”。
B. 攻击实施
- 接口抓包: 攻击者通过正常业务流程进入比赛编辑页面,抓取
PUT /game/editGame请求。 - 请求篡改: 在重放请求的 JSON Body 中,额外注入敏感字段:
"ourScore": 0, "opponentScore": 99。 - 关键证据: 发送请求后,服务端返回
500 Internal Server Error。结合日志分析,请求已成功穿透了权限拦截器和字段校验机制到达核心业务层,只是因本地数据库连接环境空指针而失败。若环境正常,恶意比分将被直接写入数据库。这证明字段过滤机制已彻底失效。
C. 安全加固 (字段白名单机制)
核心原则: 永远不要盲目信任客户端提交的完整对象。使用专门的 Update DTO + 手动字段赋值:
@Data
public class GameUpdateDto {
@NotBlank private String id;
@Size(max = 255) private String venue; // 仅允许修改场地
@Future private Date date; // 仅允许修改日期
// 明确不包含 ourScore、opponentScore 等敏感字段
}
@Override
@Transactional
public ResponseResult editGame(GameUpdateDto dto) {
Game dbGame = checkAndGetGame(dto.getId());
// 严格白名单赋值:仅更新允许客户端修改的字段
if (dto.getVenue() != null) dbGame.setVenue(dto.getVenue());
if (dto.getDate() != null) dbGame.setDate(dto.getDate());
// 敏感字段由服务端根据业务逻辑计算,禁止从客户端更新
gameMapper.updateById(dbGame);
return new ResponseResult(200, "修改成功");
}
D. 相关知识
- 批量赋值 (Mass Assignment): 框架自动将请求参数绑定到模型对象所有属性的通病。防御核心是“显式允许”而非“隐式允许”。
- OWASP 建议: API3 的防御应与 API1 结合——既要控制“谁能操作”,也要控制“能操作什么”。
🔥 案例二:俱乐部成员列表过度暴露手机号
A. 风险分析
ClubController.getPlayers() 接口在返回俱乐部成员列表时,将每个成员的 telephone 直接填充进 DTO。由于手机号同时是系统的登录账号和默认初始密码,普通球员获取后可直接尝试冒用他人身份。此漏洞本质是响应数据过度暴露:接口返回了超出调用者权限所需范围的敏感字段。
B. 攻击实施
- 发起请求: 普通球员登录获取 Token,调用
GET /club/getPlayers。 - 过度暴露: 响应体中包含了同俱乐部所有成员的
telephone字段。 - 横向利用: 攻击者提取出属于管理员的手机号,将其作为账号/密码尝试登录。若管理员未改默认密码,直接接管管理员权限。
C. 安全加固 (按角色动态过滤敏感字段)
// 1. 判断当前用户是否有权查看手机号 (仅系统管理员或本队管事可见)
boolean canViewTelephone = permissions.contains("ROLE_ADMIN")
|| (!Objects.isNull(currentClubPlayer) && "ROLE_CLUB_ADMIN".equals(currentClubPlayer.getRole()));
List<ClubPlayerDto> dtoList = new ArrayList<>();
for (ClubPlayer clubPlayer : clubPlayerList) {
ClubPlayerDto dto = new ClubPlayerDto(clubPlayer);
// ...
// 2. 根据权限动态控制响应字段
if (canViewTelephone) {
dto.setTelephone(player.getTelephone());
} else {
dto.setTelephone(null); // 无权限则置空,或使用脱敏策略 (如 180****3020)
}
dtoList.add(dto);
}
D. 相关知识
- 过度数据暴露 (Excessive Data Exposure): 开发者倾向于图省事复用底层实体类作为响应对象。防御核心是“最小数据原则”。
- 分层 DTO 与脱敏: 复杂系统中应定义
PublicDto、AdminDto并在编译期隔离字段。必须展示敏感数据时,应采用数据脱敏 (Data Masking) 策略。
API4:2023 无限制的资源消耗
风险定义: API 未对客户端的请求频率、执行超时或分配内存设置合理限制。攻击者可发送海量请求耗尽 CPU、内存、数据库连接池,导致 DoS(拒绝服务),或进行暴力破解。
⚠️ 危害与识别
- 缺乏 Rate Limiting 和并发连接数限制。
- 高消耗接口(登录/搜索)无特殊保护。
🛡️ 防范机制
- 限流算法: 使用令牌桶/漏桶(Bucket4j/Guava)。
- 分层防护: 全局限流 + 敏感接口级限流。
- 配额管理: 限制单用户上传文件数量、超时时间。
// Bucket4j 限流代码示例
if ("/user/login".equals(apiPath)) {
Bucket bucket = rateConfig.resolveBucket(clientIp, 10, 5, Duration.ofMinutes(1));
if (!bucket.tryConsume(1)) {
response.setStatus(429); // Too Many Requests
return;
}
}
🔥 案例一:登录接口高频爆破
A. 风险分析
球易系统的 POST /user/login 接口未实施任何请求频率限制。虽然项目中存在 RateLimitingConfig.java(基于 Bucket4j 的令牌桶限流配置),但该组件属于"孤岛代码"——未在 Spring Security 过滤器链、Spring Interceptor 或任何请求处理环节中被实际调用。所有请求均可直达后端,触发 CPU 密集的 BCrypt 密码哈希校验逻辑。
BCrypt 是专门设计的慢哈希算法(默认 cost factor 为 10,单次哈希约 100ms),在提供密码安全性的同时,也成为 DoS 攻击的高价值目标。攻击者无需高成本资源,仅通过普通网络连接即可对服务器造成显著负载。
B. 攻击实施
- 工具配置: 在 Postman 中打开 Runner 功能,创建针对
POST /user/login的测试集合。 - 参数设置: 迭代次数(Iterations)设为 50 次,并发延迟(Delay)设为 0ms。请求体使用
{"telephone":"18038383020","password":"任意错误密码"}。 - 发起攻击: 启动 Runner,50 次请求在不到 5 秒内全部完成。
- 关键证据: 所有请求均返回 200 OK(虽登录失败但 HTTP 状态码为 200),未触发任何 429 Too Many Requests 拦截响应。服务端 BCrypt 哈希计算被高频调用,证明系统完全缺乏限流防线,理论上可无限放大请求量直至服务瘫痪。
C. 安全加固 (全局拦截与令牌桶限流)
核心原则: 将限流组件挂载到请求处理链条的最前端,对高消耗接口实施强制拦截。
@Component
@Order(1) // 确保在 Spring Security 过滤器之前执行
public class RateLimitFilter extends OncePerRequestFilter {
@Autowired
private RateLimitingConfig rateLimitingConfig;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
String apiPath = request.getRequestURI();
// 针对登录接口实施 IP 级限流
if ("/user/login".equals(apiPath)) {
String clientIp = getClientIp(request);
// 设定容量为 10,每分钟补充 5 个令牌
Bucket bucket = rateLimitingConfig.resolveBucket(clientIp, apiPath, 10, 5, Duration.ofMinutes(1));
if (!bucket.tryConsume(1)) {
response.setStatus(429);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"status\":429,\"msg\":\"请求过于频繁,请稍后再试\"}");
return; // 令牌耗尽,直接阻断,不进入后续过滤器
}
}
chain.doFilter(request, response);
}
private String getClientIp(HttpServletRequest request) {
String ip = request.getHeader("X-Forwarded-For");
return (ip == null || ip.isEmpty()) ? request.getRemoteAddr() : ip.split(",")[0].trim();
}
}
D. 相关知识
- 令牌桶算法 (Token Bucket): 限流经典算法之一。允许一定程度的突发流量,同时保持长期平均速率。
- BCrypt 的 DoS 特性: 慢哈希设计是安全需求(抵抗离线破解),但也使其成为 DoS 目标。防御不能依赖"攻击者不会大量请求",必须依赖"系统不允许大量请求"。
- 速率限制 vs 账户锁定: 速率限制针对 IP/客户端,防资源耗尽;账户锁定针对账号凭证,防暴力破解。两者应配合构建立体防御。
- HTTP 429 状态码: RFC 6585 定义的 "Too Many Requests",可配合
Retry-After响应头告知客户端何时可以重试。
API5:2023 失效的功能级别授权 (BFLA)
风险定义: API 缺乏对功能级别的权限控制,导致普通用户可以访问管理员接口,或低权限用户可以执行高权限操作。属于典型的垂直越权问题。
⚠️ 危害与识别
- 接口缺乏
@PreAuthorize注解。 - 逻辑缺陷(如自定义表达式配置错误)。
- 管理端接口与 C 端接口未物理隔离。
🛡️ 防范机制
- 最小权限原则: 明确划分层级,使用框架拦截器管控。
- 权限审计: 自动化测试验证接口是否阻断低权访问。
🔥 案例一:MyExpressionRoot 默认放行与接口权限缺失
A. 风险分析
MyExpressionRoot.hasRole() 方法在末尾存在严重逻辑缺陷:当传入的 role 参数未匹配任何已知角色时,直接返回 true。这意味着任何拼写错误的角色名甚至空字符串,都能无条件通过权限校验。
此外,UserController 中的 /user/addUser 等敏感接口未添加 @PreAuthorize 注解,完全依赖业务层的后置校验。虽然业务层返回了 410 错误,但接口本身已向低权限用户暴露,攻击者可枚举接口并探测参数结构。
B. 攻击实施
- 接口探测: 以普通球员身份(
ROLE_CLUB_PLAYER)登录获取 Token。 - 越权访问: 调用
POST /user/addUser?telephone=...,观察到业务层返回 410 "用户无该权限",但接口本身正常响应了 HTTP 200。这证明低权限用户已成功访问管理员接口(控制器层未拦截)。 - 结合多角色: 若该用户同时拥有
ROLE_ADMIN权限,则可通过/user/switchRole切换后,直接成功执行管理员操作。
C. 安全加固 (默认拒绝与方法级安全)
// 1. 修复权限表达式:删除默认放行,改为默认拒绝
public boolean hasRole(String role) {
// ... 原有校验逻辑不变 ...
// return true; // ← 删除此行 (危险的默认放行)
return false; // ← 未匹配任何角色时坚决拒绝
}
// 2. 为所有敏感接口补充方法级前置校验
@PreAuthorize("@ex.hasRole('ROLE_ADMIN')")
@PostMapping("/user/addUser")
public ResponseResult addUser(@RequestParam String telephone) {
return userService.addUser(telephone);
}
D. 相关知识
- 默认拒绝原则 (Deny by Default): 安全设计核心原则。权限控制的方法末尾必须返回
false。 - 方法级安全 (Method Security):
antMatchers只能保护 URL 路径,无法细分 HTTP 方法和业务场景。必须结合@PreAuthorize实施深度防御。
🔥 案例二:switchRole 接口缺失功能级校验导致权限提升
A. 风险分析
switchRole() 接口未添加前置校验,业务校验逻辑仅检查目标角色是否在用户的 role_list 中,而未限制角色切换方向(允许降权后再提权)。加之 hasRole() 对 ROLE_ADMIN 存在直接放行的硬编码:if (permissions.contains("ROLE_ADMIN")) return true;。这使得攻击者可以通过 switchRole 强行将当前活跃角色(current_role)提权为 ROLE_ADMIN,从而向所有管理员接口敞开大门。
B. 攻击实施
- 主动降权: 登录包含多角色的测试账号,调用
POST /user/switchRole?roleName=ROLE_USER降权为普通用户。 - 验证拦截: 尝试调用
/user/addUser,返回 403,确认当前为低权状态。 - 恶意提权: 再次调用
POST /user/switchRole?roleName=ROLE_ADMIN,由于无前置校验,Redis 中的活跃角色被更新为最高权限。 - 攻击成功: 使用原 Token 再次调用
/user/addUser,接口成功执行,完成“不重新登录即可提权”的攻击链路。
C. 安全加固 (状态转换约束)
// 1. Controller 增加前置状态转换校验
@PreAuthorize("@ex.canSwitchToRole(#roleName)")
@PostMapping("/user/switchRole")
public ResponseResult switchRole(@RequestParam String roleName) { ... }
// 2. MyExpressionRoot 新增严谨的转换逻辑
public boolean canSwitchToRole(String targetRole) {
// ... 获取用户信息 ...
if (!roleList.contains(targetRole)) return false;
// 核心风控:禁止从低权(如 ROLE_USER)直接切回高权(ROLE_ADMIN)
String currentRole = user.getCurrentRole();
if (!"ROLE_ADMIN".equals(currentRole) && "ROLE_ADMIN".equals(targetRole)) {
return false; // 需要高权时必须要求重新登录验密
}
return true;
}
D. 相关知识
- BFLA vs BOLA: BFLA(功能级) 是垂直越权,关注“能否执行某操作”;BOLA(对象级) 是水平越权,关注“能否操作特定资源”。两者需叠加校验。
- 状态转换安全: 必须明确定义允许的状态机流转路径(如 A → B 允许,但 B → A 拒绝)。
- Redis 与 Token 同步: Token 角色固定而 Redis 可变会导致状态割裂,建议 Token 仅作身份标识,权限完全收敛至 Redis 实时查询。
🔥 案例三:权限校验空指针异常导致访问控制绕过
A. 风险分析
hasRole() 中直接调用了 playerLastClub.getLastClubId()。当用户是从未加入任何俱乐部的新用户时,查出的 playerLastClub 为 null,触发 NullPointerException。异常向上抛出会导致 Spring Boot 返回 500 Internal Server Error,这向攻击者暴露了权限校验已崩溃的事实。如果异常发生在鉴权后、业务前,攻击者甚至可能利用竞态条件完成部分攻击。
B. 攻击实施
- 构造异常账号: 注册一个新用户或删除其俱乐部关联记录。
- 触发异常: 使用该账号 Token 调用需权限的
PUT /game/editGame。 - 关键证据: 响应返回 500 并附带
hasRole(MyExpressionRoot.java:58)空指针堆栈信息。证明未正常返回 403,权限表达式求值中断,访问控制层被物理穿透。
C. 安全加固 (防御性编程与全局捕获)
// 1. hasRole 中增加防御性空值判断 (Fail-Safe Defaults)
PlayerLastClub playerLastClub = playerLastClubMapper.selectOne(...);
if (Objects.isNull(playerLastClub)) {
return false; // 未加入任何俱乐部 → 直接拒绝权限
}
// 2. 增加全局权限异常的兜底捕获 (@RestControllerAdvice)
@ExceptionHandler(NullPointerException.class)
public ResponseResult handleNPE(NullPointerException e, HttpServletRequest request) {
if (e.getStackTrace()[0].getClassName().contains("MyExpressionRoot")) {
log.error("权限校验异常, URI: {}", request.getRequestURI());
return new ResponseResult(403, "权限校验失败"); // 收敛为标准的 403 响应
}
return new ResponseResult(500, "系统繁忙");
}
D. 相关知识
- 防御性编程 (Defensive Programming): 权限校验代码是系统的南天门,对每一层数据库查询结果都必须做严苛的边界和空值校验。
- 故障安全默认 (Fail-Safe Defaults): 系统发生异常(如 DB 宕机、空指针)时,默认行为必须是“拒绝访问”而非抛出不受控的系统异常。
API6:2023 对敏感业务流的无限制访问
风险定义: API 暴露了敏感业务流(下单、投票、发帖),虽不导致宕机,但攻击者可使用脚本自动化大规模调用,造成业务逻辑滥用(如机器人刷单、刷榜)。
POST /game/addGame,瞬间生成 1000 场假数据,彻底破坏年度数据盘;或通过脚本批量注册垃圾用户。
⚠️ 危害与识别
- 关键写接口缺乏人机验证(CAPTCHA)。
- 缺乏对业务合理性的上下文检测(如频率骤增)。
🛡️ 防范机制
- 引入人机验证: 前端增加滑块或图形验证码。
- 业务逻辑限频: 单用户单日操作次数硬管控。
🔥 案例一:无限制地访问敏感业务流 (自动化刷榜)
A. 风险分析
系统在 GameController 中暴露了 /game/addGame 接口,允许俱乐部管理员新增比赛记录。该接口的实现仅仅校验了用户的身份权限(是否具备 ROLE_CLUB_ADMIN),却完全忽略了对业务流本身的保护约束。
该业务流未实现任何防自动化滥用机制(如频率限制、人机识别验证码、异常数据阈值阻断等)。攻击者可以通过编写简单的自动化脚本(Bot),绕过常规的用户界面限制,高频、批量地向数据库注入伪造的比赛数据(如单场进球 999 个的垃圾数据)。虽然这种攻击未必会造成服务器宕机,但会彻底摧毁系统的核心业务价值——导致基于比赛数据计算的年度统计报表(ClubStatisticsDto 中的胜负场、总进球数等)完全失真。
B. 攻击实施
- 准备凭证: 攻击者以俱乐部管理员身份登录,获取合法的鉴权 Token。
- 构造载荷: 在 Postman 中构造
POST /game/addGame的单次请求,并在 Body 中填入严重偏离常理的恶意 JSON 载荷:
{
"year": 2026,
"ourTeamId": "1760925290416164864",
"opponentTeamName": "自动化水军机器队",
"ourScore": 999,
"opponentScore": 0,
"eventId": "1762371996437528576",
"venue": "刷分测试球场"
}
- 高频重放: 利用 Postman Runner 自动化测试工具,将该请求的执行迭代次数(Iterations)设置为 100 次,模拟脚本高频并发提交。
- 关键证据: Runner 执行结束,全部 100 次请求均返回
200 OK。随后调用/game/getClubStatistics/2026接口,发现该俱乐部的总进球数瞬间激增近 10 万个,系统的核心竞技统计功能被成功滥用并彻底破坏。
C. 安全加固 (业务风控与人机识别)
防范 API6 需要从业务逻辑和人机识别双管齐下:
- 基于业务属性的频次限制 (Business-Logic Rate Limiting): 在
GameService中增加逻辑校验,限制单一俱乐部或单一管理员每天最多只能提交 5 场比赛记录,超出部分需次日提交或人工审批。 - 异常阈值阻断: 在反序列化
GameDto后,增加对ourScore和opponentScore等业务字段的合理性阈值判定(如篮球单场比分一般不超过 200,足球不超过 20),拦截离谱的垃圾数据。 - 引入人机交互验证 (CAPTCHA): 在前端提交表单时,强制要求完成滑动验证码或行为分析验证,确保调用
addGame接口的主体是真实人类而非自动化脚本。
D. 相关知识
- API6 vs API4: 开发人员和安全测试人员常将两者混淆。API4 的攻击目标是耗尽服务器计算资源导致宕机;而 API6 的目标是滥用业务逻辑以获取利益或造成破坏。
- 零信任架构对机器的评估: 在 API6 场景中,后端服务器通常运行完美,毫无报错,但业务流已被机器脚本彻底操控。零信任不仅要求对“人”进行身份验证,还要对“机器和行为流”进行合法性评估。
API7:2023 服务端请求伪造 (SSRF)
风险定义: 攻击者通过精心构造请求,诱使后端服务器去访问其本不应该访问的内部或外部资源(如内网管理口、云主机元数据)。
http://169.254.169.254/latest/meta-data,服务器向自身云环境发请求,泄露临时最高凭证。
⚠️ 危害与识别
- 后端接收外部 URL 并主动通过 HTTP 客户端发起请求。
- 未验证目标解析后的真实 IP 是否属于内网。
🛡️ 防范机制
- URL 白名单: 限制可信域名,禁止自动重定向。
- 内网阻断: 封禁对 127.0.0.1、192.168.x.x 等地址的访问。
private boolean isValidUrl(String url) {
// 正则拒绝内网地址段
if (url.matches(".*(127\\.0\\.0\\.1|localhost|10\\.|172\\.(1[6-9]|2[0-9]|3[01])\\.|192\\.168\\.|169\\.254\\.).*")) {
return false;
}
return true;
}
🔥 案例一:Webhook 探测内网敏感凭证 (SSRF)
A. 风险分析
系统在 ClubServiceImpl.testWebhook() 方法中添加了供俱乐部管理员配置通知 Webhook 的测试功能。代码缺陷在于:
String response = restTemplate.getForObject(webhookUrl, String.class);
系统直接从客户端接收 webhookUrl 参数,并在未经任何白名单验证和内网 IP 阻断的情况下,利用后端的 RestTemplate 发起了 HTTP 请求。这种“无脑代发”行为导致系统容易遭受 SSRF 攻击。攻击者可利用服务器作为跳板,探测或攻击企业内网(Intranet)的其他脆弱服务,甚至读取云服务器元数据(如 AWS 的 169.254.169.254)。
B. 攻击实施
- 获取凭证: 攻击者以俱乐部管理员身份登录,获取合法 Token。
- 构造载荷: 构造恶意请求,尝试探测系统内网中隐藏的免鉴权配置接口。发送
POST /club/testWebhook,将webhookUrl参数设置为内网目标:http://127.0.0.1:8095/internal/secret。 - 关键证据: 观察响应,服务器作为代理,成功向本地内网发起了请求,并将包含数据库密码和云主机凭证的敏感 JSON 数据通过接口直接返回给了攻击者,造成严重的内网资产泄露。
C. 安全加固 (URL 白名单与 DNS 底层校验)
在向外发起网络请求前,必须实现严格的 URL 白名单与目标地址过滤机制:
- 解析与校验: 解析传入的 URL,检查 Host 是否在预设的可信域名白名单中。
- 禁用内网 IP: 如果是用户自定义的外部 URL,通过 DNS 解析目标域名获取真实 IP,并严格检查该 IP 是否属于内网保留地址段(如 127.0.0.0/8, 192.168.0.0/16, 10.0.0.0/8 等)。如果解析到内网 IP 则立即阻断。
- 禁用敏感协议: 限制仅允许使用
http://和https://协议,禁止file://、gopher://等可能被用于读取本地文件或反弹 Shell 的危险协议。
在利用 RestTemplate 发起请求前,增加校验拦截器:
import java.net.InetAddress;
import java.net.URI;
// 在 ClubServiceImpl 中修复 testWebhook 方法:
@Override
public ResponseResult testWebhook(String webhookUrl) {
try {
URI uri = new URI(webhookUrl);
// 1. 协议白名单校验:只允许 http 和 https
String scheme = uri.getScheme();
if (!"http".equalsIgnoreCase(scheme) && !"https".equalsIgnoreCase(scheme)) {
return new ResponseResult(403, "Webhook 配置失败:仅支持 HTTP/HTTPS 协议");
}
// 2. DNS 解析并进行内网 IP 阻断校验
InetAddress inetAddress = InetAddress.getByName(uri.getHost());
// isLoopbackAddress: 拦截 127.0.0.1 等环回地址
// isSiteLocalAddress: 拦截 192.168.x.x, 10.x.x.x 等局域网私有地址
// isAnyLocalAddress / isLinkLocalAddress: 拦截其他特殊本地路由
if (inetAddress.isLoopbackAddress() || inetAddress.isSiteLocalAddress() ||
inetAddress.isAnyLocalAddress() || inetAddress.isLinkLocalAddress()) {
return new ResponseResult(403, "Webhook 配置失败:禁止访问内网或本地保留地址!");
}
// 3. 校验通过,发起安全请求
String response = restTemplate.getForObject(uri, String.class);
return new ResponseResult(200, "Webhook 测试连通性成功,响应内容:", response);
} catch (Exception e) {
return new ResponseResult(500, "Webhook 测试失败或 URL 格式非法:" + e.getMessage());
}
}
D. 相关知识
- 信任边界的破坏: 现代微服务和云原生架构中,许多内部 API 和基础设施(如 Kubernetes API, 云厂商 Meta-data 服务)默认请求若来自内网机器即为合法,从而省略了强身份认证。SSRF 正是打破了这一假设,让外网黑客可以“借刀杀人”,利用拥有内网通行证的 Web 服务器来刺探并攻击脆弱的内部基础设施。
API8:2023 安全配置错误
风险定义: API 或其依赖组件(如 Spring、Redis、FastJson)的配置存在安全缺陷,导致系统暴露。也是文件上传逻辑缺陷的重灾区。
⚠️ 危害与识别
- 代码中数据库密码、JWT Secret 硬编码(如本项目)。
- CORS 配置过度宽松(
allowedOrigins("*"))。 - 暴露堆栈报错信息给客户端。
🛡️ 防范机制
- 密钥分离: 使用环境变量
${DB_PASSWORD}读取。 - 严格 CORS: 指定明确的前端请求域名白名单。
🔥 案例一:文件上传后缀校验逻辑错误
A. 风险分析
FileController.upload() 方法的文件类型校验代码存在严重的布尔表达式构建错误:
if (!ext.equals(".jpg") && !ext.equals(".png") && ext.equals(".jpeg"))
第三个条件使用了正向等于 ext.equals(".jpeg") 而非反向不等于 !ext.equals(".jpeg"),导致逻辑完全颠倒。实际效果为:只要不是 .jpg 或 .png,且恰好是 .jpeg,才拒绝。由于 .jpeg 同时满足"不是 .jpg"和"不是 .png",最终只有 .jpeg 被错误拦截,而 .gif、.jsp、.exe、.sh 等所有其他格式均被放行。这使得攻击者可以轻易上传 WebShell,夺取服务器控制权。
B. 攻击实施
- 准备凭证: 登录系统获取合法 Token。
- 实施上传: 使用 Postman 发送
POST /file/upload,Header 携带 Token,Body 使用 form-data。选择一个包含恶意脚本的.jsp文件。 - 关键证据: 观察响应返回
200 上传成功,危险文件未被拦截,成功存入服务器image/test123/目录,为后续远程代码执行(RCE)创造条件。
C. 安全加固 (引入白名单集合)
// 1. 在类顶部定义不可变的白名单常量集合
private static final Set<String> ALLOWED_EXTENSIONS = new HashSet<>(Arrays.asList("jpg", "jpeg", "png"));
// 2. 替换原有脆弱的 IF 校验逻辑
String originalFilename = file.getOriginalFilename();
String ext = originalFilename.substring(originalFilename.lastIndexOf(".") + 1).toLowerCase();
if (!ALLOWED_EXTENSIONS.contains(ext)) {
return new ResponseResult(512, "仅支持 jpg、jpeg、png 格式");
}
D. 相关知识
- 布尔表达式短路逻辑: Java 中
&&具有短路特性。原代码的实际真值表导致防御失效。防御核心是使用白名单集合而非复杂的布尔表达式,并通过单元测试覆盖所有边界情况。
🔥 案例二:文件下载路径遍历 (Path Traversal)
A. 风险分析
FileController.download() 方法将用户传入的 fileName 参数直接拼接到服务器绝对路径中:
FileInputStream fileInputStream = new FileInputStream(new File(pre + fileName));
未进行路径规范化与校验,攻击者可通过 ../ 或绝对路径跳出预定的 image 目录,读取服务器上的任意文件,如系统配置文件(win.ini / /etc/passwd)、数据库凭证、源代码等敏感信息。
B. 攻击实施
- 发起请求: 登录获取 Token。发送
GET /file/download?fileName=C:/windows/win.ini(或使用../../../../跳出目录)。 - 关键证据: 响应直接返回了操作系统底层文件
win.ini的内容,证明路径遍历成功,服务器任意文件可被读取。
C. 安全加固 (规范化校验)
// 1. 基础路径安全拦截
private boolean isValidFilePath(String fileName) {
if (fileName == null || fileName.isEmpty()) return false;
if (fileName.contains("..") || fileName.contains("\\")) return false; // 拦截跨目录符
return fileName.startsWith("/image/");
}
// 2. 底层 getCanonicalFile 终极防护
File targetFile = new File(basePath, fileName).getCanonicalFile(); // 解析为系统真实绝对路径
File baseDir = new File(basePath, "image").getCanonicalFile();
if (!targetFile.getPath().startsWith(baseDir.getPath())) {
response.setStatus(403);
response.getWriter().write("非法文件路径,禁止越权读取");
return;
}
🔥 案例三:文件删除路径遍历
A. 风险分析
FileController.deleteGameImage() 方法同样将用户传入的 fileName 直接拼接实例化 new File(dirPath + fileName)。与下载接口类似,缺乏路径校验。攻击者可通过构造恶意参数删除服务器上的任意文件,导致系统可用性破坏或严重数据丢失。
B. 安全加固
@DeleteMapping("/file/deleteGameImage")
@PreAuthorize("@ex.hasRole('ROLE_CLUB_ADMIN')")
public ResponseResult deleteGameImage(@RequestParam String fileName) {
if (!isValidFilePath(fileName)) return new ResponseResult(403, "非法文件路径");
File file = new File(dirPath + fileName);
try {
// 二次校验:确保将要删除的文件被死死锁在 image 目录下
if (!file.getCanonicalPath().startsWith(new File(dirPath, "image").getCanonicalPath())) {
return new ResponseResult(403, "非法文件路径");
}
} catch (IOException e) {
return new ResponseResult(500, "路径解析错误");
}
return file.delete() ? new ResponseResult(200, "删除成功") : new ResponseResult(513, "删除失败");
}
C. 相关知识
- 删除操作的安全边界: 文件删除是不可逆操作,比读取更具破坏性。防御除路径校验(如
getCanonicalPath)外,还应增加操作审计日志(记录删除者、时间、路径),以及推行软删除机制(逻辑删除标记,保留恢复能力),降低误操作和恶意删除的影响。
API9:2023 不当的资产管理
风险定义: 旧版本漏洞接口未下线、开发测试接口被暴露到外网(影子 API),导致防护体系被绕过。
@GetMapping("/test/userList") 测试接口,未挂载权限校验,黑客扫描到后直接拖库。
⚠️ 危害与识别
- 无 Swagger 或完整 API 生命周期文档。
- 存在废弃的
/v1/接口仍在服役。
🛡️ 防范机制
- 自动化文档: 强制集成 OpenAPI/Swagger。
- 定期审计清理: 上线前剔除所有调试接口。
🔥 案例一:测试接口未删除与 API 版本缺失
A. 风险分析
球易系统存在两处典型的资产管理问题:
1. 测试接口未删除: UserController.hello() 是一个开发阶段用于调试的接口:
@GetMapping("/hello")
public void hello() {
UsernamePasswordAuthenticationToken authentication = (UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
System.out.println(loginUser.getUser().getUsername());
}
该接口未添加任何权限注解(无 @PreAuthorize)。它直接打印内部用户信息到控制台。若开发者忘记删除,此类接口往往缺乏防备,极易成为攻击者探测系统内部结构的入口(影子 API)。
2. API 缺乏版本控制:
系统所有接口均无版本前缀(如 /v1/user/login),直接暴露在根路径下。这导致系统升级时无法平滑过渡(旧接口必须强制替换),且缺乏标准的接口清单,运维人员根本无法掌握系统真实的暴露面有多大。
B. 攻击实施
- 探测系统状态: 攻击者携带任意有效 Token 访问
GET /hello。虽然 HTTP 响应为空(void),但未报 404/403,请求被正常处理。攻击者借此推断系统存在未文档化的“影子接口”,进而利用 DirBuster 等工具枚举出更多高危隐藏路径。 - 寻找安全盲区: 攻击者通过反编译 jar 包获取真实路由清单,对比系统的官方文档(假设有),挑出那些文档上没写、开发者早就遗忘的接口(如
/user/switchRole、/hello)进行定点打击。
C. 安全加固 (版本前缀与 Swagger 清单)
1. 删除测试接口或改为安全健康探针: (至少要求 @PreAuthorize("isAuthenticated()") 并返回通用状态)
2. 建立 API 版本控制与自动化文档生命周期管理:
// 新增全局版本前缀常量
public class ApiVersion { public static final String V1 = "/api/v1"; }
// Controller 统一加上版本号映射
@RestController
@RequestMapping(ApiVersion.V1 + "/user")
public class UserController { ... }
// 使用 Swagger/OpenAPI 自动生成接口清单,告别黑盒
@Bean
public Docket api() {
return new Docket(DocumentationType.OAS_30)
.apis(RequestHandlerSelectors.basePackage("com.micer.easyball.controller"))
.build()
.apiInfo(new ApiInfoBuilder().title("EasyBall API 清单").version("v1.0.0").build());
}
D. 相关知识
- 影子 API (Shadow API): 未在官方文档中记录、未经过安全审查但实际可访问的 API。来源包括:测试接口未删除、功能迭代遗留、第三方组件自带接口。这是黑客的重点提款机。
- API 清单 (API Inventory): 企业安全治理的基础。必须登记接口路径、HTTP 方法、权限要求、数据敏感度、负责人及生命周期。
- API 版本控制策略: URL 路径版本(
/v1/users)最直观常用;也可通过 Header(Accept: application/vnd.v1+json)或参数实现。 - 安全下线规范: 旧版本 API 绝不能直接物理删除导致客户端崩溃。应遵循:标为
Deprecated→ 监控调用量并通知迁移 → 约定时间到期返回410 Gone或404→ 最终从代码层面移除。
API10:2023 API 的不安全消费
风险定义: 系统在调用第三方外部 API(如天气接口、支付回调)时,默认对方是绝对安全的,未对返回响应做防御性过滤,导致引狼入室。
⚠️ 危害与识别
- 直接将第三方 API 的返回内容透传给前端渲染。
- 攻击者劫持第三方 API,注入 XSS 载荷或脏数据。
🛡️ 防范机制
- 零信任原则: 对一切外部输入进行类型强校验和 HTML 转义。
- 熔断机制: 防止外部服务瘫痪拖垮主服务。
🔥 案例:调用第三方服务时对数据盲目信任 (XSS 注入)
A. 风险分析
WeatherServiceImpl.getWeather() 方法在调用第三方气象服务时存在数据盲目信任与处理逻辑缺陷:
String response = restTemplate.getForObject(WEATHER_API_URL, String.class);
JSONObject jsonObject = JSON.parseObject(response);
String argsStr = jsonObject.getString("args");
return JSON.parseObject(argsStr, WeatherDto.class);
代码直接将从外部 URL 获取的 JSON 字符串反序列化为内部的 WeatherDto 对象,并直接将其封装进 ResponseResult 透传返回给前端。整个过程完全缺失了针对第三方数据的白名单校验和危险字符清理(Sanitization)。这意味着一旦第三方 API 被攻破、发生 DNS 投毒,或者出现中间人劫持,攻击者就可以向系统注入恶意载荷(如 <script> 标签)。系统不仅未能拦截,反而充当了恶意代码的“搬运工”,最终导致 Web 前端在渲染这些气象信息时触发存储型跨站脚本攻击(XSS)。
B. 攻击实施
- 劫持第三方源: 攻击者控制 DNS 或发起中间人攻击,将
api.weather.com解析到恶意服务器。 - 伪造响应载荷: 恶意服务器返回伪造的 JSON 响应,并在
alert字段中注入<script>alert('XSS')</script>。 - 触发业务调用: 重启服务,使用 Postman 发送
GET /game/getGameDetail/9999999999999999999,Header 携带合法 Token。 - 关键证据: 观察响应,系统返回
200 OK,且 JSON 体中的alert字段原封不动地携带了未转义的<script>恶意标签。攻击载荷成功穿透后端防线,直达客户端。
C. 安全加固 (防御性净化 Sanitization)
在反序列化并赋值前,利用 Spring 框架自带的 HTML 转义工具,对来自第三方的不可信字符串类型数据进行严格的防御性净化:
import org.springframework.web.util.HtmlUtils;
private WeatherDto parseResponse(String response) {
JSONObject jsonObject = JSON.parseObject(response);
String argsStr = jsonObject.getString("args");
// 1. 解析为不受信任的原始对象
WeatherDto rawWeather = JSON.parseObject(argsStr, WeatherDto.class);
// 2. 核心防御:创建一个新的安全对象,并对所有字符串字段进行 HTML 转义
WeatherDto safeWeather = new WeatherDto();
// 将潜在的危险字符(如 <, >, &, ")转换为 HTML 实体(如 <, >)
safeWeather.setTemperature(HtmlUtils.htmlEscape(rawWeather.getTemperature()));
safeWeather.setCondition(HtmlUtils.htmlEscape(rawWeather.getCondition()));
safeWeather.setIconUrl(HtmlUtils.htmlEscape(rawWeather.getIconUrl()));
safeWeather.setAlert(HtmlUtils.htmlEscape(rawWeather.getAlert()));
return safeWeather;
}
D. 相关知识
- 零信任原则与 API 数据消费: OWASP API10 指出,开发团队常常默认外部服务(尤其是知名或商业第三方 API)是绝对安全的,从而对它们返回的数据放宽了输入验证和输出编码的标准。
- 跨越信任边界: 在零信任(Zero Trust)架构下,任何跨越信任边界的数据都应被视为潜在的威胁。无论是前端用户的直接输入,还是其他微服务、第三方 API 接口的响应,在参与系统内部计算或发送至客户端渲染之前,都必须经过统一的强类型校验与转义过滤。
F. 资源下载
📥 靶场源码与环境资源
包含 Easyball 原始漏洞环境源码、数据库初始化 SQL 脚本,以及经过架构级安全加固后的最终版代码集合。
(注:点击按钮目前仅作演示,待文件上传至服务器或云盘后,替换 a 标签中的 href 链接即可)
华南理工大学·软件学院
版权所有·违版必究