9.6 KiB
P0 漏洞修复 — 给 Zcode 的执行指令
来源:渗透测试报告 v1.0(2026-09-02)
目标:上线前必须完成的 4 个严重漏洞修复
注意:以下所有修改只涉及指定文件的指定行,请勿改动其他代码
【C-01】修复 extractOperatorId() — 返回真实 operatorId
问题
CheckinController.java、AccountOperatorController.java、OperatorController.java 中,extractOperatorId() 直接返回原始 token 字符串而非解析出的 operatorId,导致路径参数中的目标 Operator ID 不受任何校验,攻击者可冒充任意 Operator。
修复方案
所有使用 extractOperatorId() 的地方,统一改为调用 jwtTokenService.getUserIdFromToken(token) 解析真实 ID。
具体修改
文件 1:activity/controller/CheckinController.java
当前代码(第 104-110 行):
private String extractOperatorId(String authHeader) {
String token = extractToken(authHeader);
return token;
}
改为:
private String extractOperatorId(String authHeader) {
String token = extractToken(authHeader);
return jwtTokenService.getUserIdFromToken(token);
}
文件 2:account/controller/AccountOperatorController.java
当前代码(第 62-67 行):
private String extractOperatorId(String authHeader) {
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
throw new RuntimeException("未认证");
}
return authHeader.substring(7);
}
改为:
private String extractOperatorId(String authHeader) {
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
throw new RuntimeException("未认证");
}
String token = authHeader.substring(7);
return jwtTokenService.getUserIdFromToken(token);
}
⚠️ 注意:该文件需要注入
JwtTokenService,检查顶部是否有:private final JwtTokenService jwtTokenService;如果没有,添加字段注入。
文件 3:operator/controller/OperatorController.java
当前代码(第 78-83 行):
private String extractId(String authHeader) {
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
throw new RuntimeException("未认证");
}
return authHeader.substring(7);
}
改为:
private String extractId(String authHeader) {
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
throw new RuntimeException("未认证");
}
String token = authHeader.substring(7);
return jwtTokenService.getUserIdFromToken(token);
}
⚠️ 注意:该文件需要注入
JwtTokenService,检查顶部是否有:private final JwtTokenService jwtTokenService;如果没有,添加字段注入。
【C-02】实现 PermissionFilter 权限校验逻辑
问题
PermissionFilter.java 第 57 行:chain.doFilter(request, response); 无条件放行,所有 Operator 接口无需检查实际权限,只要有 JWT 即可访问。
修复方案
读取请求路径所需的权限码,与数据库中 Operator 的权限列表比对。
具体修改
文件:shared/filter/PermissionFilter.java
当前代码(doFilterInternal 方法末尾):
// TODO: Phase 02 后续完善权限校验逻辑
// 目前简化:只要通过 JWT 认证即允许访问 Operator 接口
chain.doFilter(request, response);
改为:
// 从 PATH_PERMISSION_MAP 获取路径对应的必需权限码
String requiredPermission = null;
for (Map.Entry<String, String> entry : PATH_PERMISSION_MAP.entrySet()) {
if (pathMatcher.match(entry.getKey(), request.getRequestURI())) {
requiredPermission = entry.getValue();
break;
}
}
if (requiredPermission == null) {
chain.doFilter(request, response);
return;
}
// 从 request attribute 获取 operatorId(由 JwtAuthFilter 设置)
String operatorId = (String) request.getAttribute("currentOperatorId");
if (operatorId == null) {
chain.doFilter(request, response);
return;
}
// 获取 Operator 的权限列表
List<String> permissions = permissionService.getPermissionsByOperatorId(operatorId);
if (permissions == null || !permissions.contains(requiredPermission)) {
response.setContentType("application/json;charset=UTF-8");
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
response.getWriter().write("{"success":false,"code":"FORBIDDEN","message":"权限不足","data":null}");
return;
}
chain.doFilter(request, response);
同时在文件顶部 import 添加(如没有):
import jakarta.servlet.http.HttpServletResponse;
import java.util.List;
【C-03】WebSocket 连接时验证 Token
问题
ChatWebSocketHandler.java 的 afterConnectionEstablished 方法中,从 URL query string 获取 userId 和 token 后,token 未被验证。
具体修改
文件:messaging/websocket/ChatWebSocketHandler.java
找到 afterConnectionEstablished 方法(约第 30-55 行),将整个方法替换为:
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
String queryString = session.getUri().getQuery();
String userId = null;
String token = null;
if (queryString != null) {
for (String param : queryString.split("&")) {
String[] parts = param.split("=");
if (parts.length == 2) {
if ("userId".equals(parts[0])) userId = URLDecoder.decode(parts[1], StandardCharsets.UTF_8);
if ("token".equals(parts[0])) token = URLDecoder.decode(parts[1], StandardCharsets.UTF_8);
}
}
}
if (userId == null || token == null) {
session.close(CloseStatus.NOT_ACCEPTABLE);
return;
}
// 验证 Token 有效性
if (!messagingService.validateToken(token)) {
log.warn("WebSocket Token 验证失败: userId={}", userId);
session.close(CloseStatus.NOT_ACCEPTABLE);
return;
}
// 验证 userId 与 token 中解析出的 userId 一致
String verifiedUserId = messagingService.getUserIdFromToken(token);
if (!verifiedUserId.equals(userId)) {
log.warn("WebSocket userId 不匹配: urlUserId={}, tokenUserId={}", userId, verifiedUserId);
session.close(CloseStatus.NOT_ACCEPTABLE);
return;
}
sessionUserMap.put(session.getId(), userId);
userSessionMap.put(userId, session.getId());
log.info("WebSocket连接建立: userId={}, sessionId={}", userId, session.getId());
sendToUser(userId, Map.of("type", "connected", "userId", userId));
}
⚠️ 注意:
MessagingService需要新增两个方法:
validateToken(String token)→ 调用jwtTokenService.validateToken(token)getUserIdFromToken(String token)→ 调用jwtTokenService.getUserIdFromToken(token)或者直接在
ChatWebSocketHandler中注入JwtTokenService。
在 ChatWebSocketHandler.java 中注入 JwtTokenService
在类字段区添加(如没有):
private final JwtTokenService jwtTokenService;
在构造方法参数中添加:
public ChatWebSocketHandler(MessagingService messagingService, JwtTokenService jwtTokenService) {
this.messagingService = messagingService;
this.jwtTokenService = jwtTokenService;
this.objectMapper = new ObjectMapper();
}
在 afterConnectionEstablished 方法顶部添加 import:
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
【C-04】生产环境 JWT 密钥配置(环境/运维任务,非代码修改)
问题
dev/staging 环境的 JWT 密钥是公开可预测的字符串。如果生产 .env 未覆盖默认值,会导致攻击者可伪造任意 Token。
执行步骤
Step 1:生成强密钥
在本地或任意安全环境中运行:
python3 -c "import secrets, base64; print('JWT_ACCESS_SECRET=' + base64.urlsafe_b64encode(secrets.token_bytes(80)).rstrip(b'=').decode())"
python3 -c "import secrets, base64; print('JWT_REFRESH_SECRET=' + base64.urlsafe_b64encode(secrets.token_bytes(80)).rstrip(b'=').decode())"
或将以下内容保存为 gen_keys.py 后运行:
import secrets, base64
print("JWT_ACCESS_SECRET=" + base64.urlsafe_b64encode(secrets.token_bytes(80)).rstrip(b'=').decode())
print("JWT_REFRESH_SECRET=" + base64.urlsafe_b64encode(secrets.token_bytes(80)).rstrip(b'=').decode())
print("CHECKIN_TOKEN_SECRET=" + base64.urlsafe_b64encode(secrets.token_bytes(80)).rstrip(b'=').decode())
Step 2:更新生产 .env 文件
将生成的密钥添加到生产环境的 .env 文件(注意:此文件不进 Git):
JWT_ACCESS_SECRET=<上面生成的字符串,107字符>
JWT_REFRESH_SECRET=<上面生成的字符串,107字符>
CHECKIN_TOKEN_SECRET=<上面生成的字符串,107字符>
⚠️ 警告:
.env文件必须通过安全渠道传递(如密码管理器、加密压缩包),禁止通过微信、QQ等明文传输。
Step 3:验证
部署后检查 Spring Boot 日志或启动输出,确认使用的是外部注入的密钥(而非 changeme-...)。
验证清单
修复完成后,请逐一验证以下场景:
| 漏洞 | 验证方法 |
|---|---|
| C-01 | 使用 Operator A 的 Token 调用 PUT /operator/OPERATOR_B_ID/disable,应返回 403 |
| C-02 | 使用无 super_admin 权限的 Operator Token 调用 POST /operator/operators,应返回 403 |
| C-03 | 使用伪造 userId 的 WebSocket 连接,应被拒绝(CloseStatus.NOT_ACCEPTABLE) |
| C-04 | 检查 .env 或启动参数中 JWT_SECRET 是否为随机强密钥(而非 dev-access-secret...) |