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

303 lines
9.6 KiB
Markdown
Raw Normal View History

2026-09-09 22:05:00 +08:00
# 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...`) |