# 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 行):** ```java private String extractOperatorId(String authHeader) { String token = extractToken(authHeader); return token; } ``` **改为:** ```java private String extractOperatorId(String authHeader) { String token = extractToken(authHeader); return jwtTokenService.getUserIdFromToken(token); } ``` --- #### 文件 2:`account/controller/AccountOperatorController.java` **当前代码(第 62-67 行):** ```java private String extractOperatorId(String authHeader) { if (authHeader == null || !authHeader.startsWith("Bearer ")) { throw new RuntimeException("未认证"); } return authHeader.substring(7); } ``` **改为:** ```java private String extractOperatorId(String authHeader) { if (authHeader == null || !authHeader.startsWith("Bearer ")) { throw new RuntimeException("未认证"); } String token = authHeader.substring(7); return jwtTokenService.getUserIdFromToken(token); } ``` > ⚠️ 注意:该文件需要注入 `JwtTokenService`,检查顶部是否有: > ```java > private final JwtTokenService jwtTokenService; > ``` > 如果没有,添加字段注入。 --- #### 文件 3:`operator/controller/OperatorController.java` **当前代码(第 78-83 行):** ```java private String extractId(String authHeader) { if (authHeader == null || !authHeader.startsWith("Bearer ")) { throw new RuntimeException("未认证"); } return authHeader.substring(7); } ``` **改为:** ```java private String extractId(String authHeader) { if (authHeader == null || !authHeader.startsWith("Bearer ")) { throw new RuntimeException("未认证"); } String token = authHeader.substring(7); return jwtTokenService.getUserIdFromToken(token); } ``` > ⚠️ 注意:该文件需要注入 `JwtTokenService`,检查顶部是否有: > ```java > private final JwtTokenService jwtTokenService; > ``` > 如果没有,添加字段注入。 --- ## 【C-02】实现 PermissionFilter 权限校验逻辑 ### 问题 `PermissionFilter.java` 第 57 行:`chain.doFilter(request, response);` 无条件放行,所有 Operator 接口无需检查实际权限,只要有 JWT 即可访问。 ### 修复方案 读取请求路径所需的权限码,与数据库中 Operator 的权限列表比对。 ### 具体修改 #### 文件:`shared/filter/PermissionFilter.java` **当前代码(`doFilterInternal` 方法末尾):** ```java // TODO: Phase 02 后续完善权限校验逻辑 // 目前简化:只要通过 JWT 认证即允许访问 Operator 接口 chain.doFilter(request, response); ``` **改为:** ```java // 从 PATH_PERMISSION_MAP 获取路径对应的必需权限码 String requiredPermission = null; for (Map.Entry 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 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 添加(如没有):** ```java 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 行),将整个方法替换为:** ```java @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 **在类字段区添加(如没有):** ```java private final JwtTokenService jwtTokenService; ``` **在构造方法参数中添加:** ```java public ChatWebSocketHandler(MessagingService messagingService, JwtTokenService jwtTokenService) { this.messagingService = messagingService; this.jwtTokenService = jwtTokenService; this.objectMapper = new ObjectMapper(); } ``` **在 `afterConnectionEstablished` 方法顶部添加 import:** ```java import java.net.URLDecoder; import java.nio.charset.StandardCharsets; ``` --- ## 【C-04】生产环境 JWT 密钥配置(环境/运维任务,非代码修改) ### 问题 dev/staging 环境的 JWT 密钥是公开可预测的字符串。如果生产 `.env` 未覆盖默认值,会导致攻击者可伪造任意 Token。 ### 执行步骤 #### Step 1:生成强密钥 在本地或任意安全环境中运行: ```bash 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` 后运行: ```python 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): ```bash 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...`) |