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

303 lines
9.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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<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 添加(如没有):**
```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...`) |