Files
danyang-match/docs/P0漏洞修复执行指令_zcode.md
T

9.6 KiB
Raw Blame History

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...)