Files
danyang-match/docs/渗透测试报告_v1.0.md
T

390 lines
15 KiB
Markdown
Raw Normal View History

2026-09-09 22:05:00 +08:00
# 🔴 丹阳公益婚恋服务平台 — 渗透测试报告
> **项目:** 丹阳公益婚恋服务平台
> **测试日期:** 2026-09-02
> **测试范围:** 后端 API、认证机制、权限控制、WebSocket、配置文件
> **测试方法:** 代码静态分析 + 架构审查(未进行实际攻击)
---
## 📋 项目技术架构
| 层面 | 技术栈 |
|------|--------|
| 后端 | Java 21 + Spring Boot 3.2.5 + PostgreSQL + Redis |
| 小程序 | 微信小程序原生 + TypeScript |
| 管理后台 | Vue 3 + Vite + Pinia + Element Plus |
| 网关 | Nginx(速率限制) |
| 部署 | Docker + Docker Compose |
---
## 🚨 漏洞总览
| 等级 | 编号 | 漏洞名称 | 影响 |
|------|------|---------|------|
| 🔴 严重 | C-01 | Operator 接口 IDOR | 可冒充任意 Operator 操作 |
| 🔴 严重 | C-02 | Operator 权限 Filter 完全旁路 | 所有 Operator 接口无权限校验 |
| 🔴 严重 | C-03 | WebSocket 未验证 Token | 可假冒任意用户接收消息 |
| 🔴 严重 | C-04 | JWT 密钥硬编码 + 可预测 | 可伪造任意用户/Operator Token |
| 🟠 高危 | H-01 | 任意用户公开资料 IDOR | 无需登录即可枚举所有用户资料 |
| 🟠 高危 | H-02 | CORS 配置错误 | 允许任意来源跨域 |
| 🟠 高危 | H-03 | 核心写操作无幂等 Key | 网络重放可产生重复记录 |
| 🟠 高危 | H-04 | Refresh Token 泄露后无法完全撤销 | 14天有效期内可被滥用 |
| 🟠 高危 | H-05 | 操作审计日志无有效身份 | 安全事件无法溯源 |
| 🟡 中危 | M-01 | JWT Access Token 无 Blacklist | 登出后 Token 在过期前仍有效 |
| 🟡 中危 | M-02 | 文件上传安全检查未实现 | Phase 10 TODO 遗留 |
| 🟡 中危 | M-03 | 缺少安全响应头 | 浏览器安全机制未启用 |
| 🟡 中危 | M-04 | AES 密钥长度不足 | 16 bytes 而非 32 bytes |
| 🟡 中危 | M-05 | Operator 账号可枚举 | 便于钓鱼攻击 |
| 🟡 中危 | M-06 | 无登录失败锁定 | 暴力猜解无防护 |
| 🟢 低危 | L-01 | Actuator 端点未限权 | 指标信息暴露 |
| 🟢 低危 | L-02 | 日志脱敏不完整 | 敏感信息可能写入日志 |
| 🟢 低危 | L-03 | Redis 无密码认证 | Redis 服务可被内网访问 |
---
## 🔴 严重漏洞(Critical)
---
### 【C-01】Operator 接口 IDOR — 可冒充任意 Operator
**文件路径:** `backend/src/main/java/com/danyang/match/platform/activity/controller/CheckinController.java`
**漏洞行号:** 第 107 行
**漏洞代码:**
```java
private String extractOperatorId(String authHeader) {
String token = extractToken(authHeader); // 提取 Bearer token 字符串
return token; // ❌ 直接返回原始 token,而非解析出的 operatorId
}
```
**对比正确实现(`AuthController.java`):**
```java
// ✅ 正确:从 JWT 中解析 userId
String userId = jwtTokenService.getUserIdFromToken(token);
```
**根因:** `extractOperatorId()` 返回的是 `Bearer eyJhbG...` 中的原始 token 字符串,而非通过 `jwtTokenService.getUserIdFromToken(token)` 解析出的 operatorId。后续调用方收到的是无意义的字符串,但代码并未真正使用该值做查询,导致**路径参数中的 Operator ID 不受任何校验**。
**影响范围:** 所有使用 `extractOperatorId()` 的 Operator 端接口:
| 受影响文件 | 受影响接口 | 后果 |
|-----------|-----------|------|
| `AccountOperatorController.java` | `POST /operator/account/{userId}/close` | 可强制注销**任意用户**账号 |
| `AccountOperatorController.java` | `GET /operator/account/{userId}/can-withdraw` | 可查询**任意用户**注销资格 |
| `OperatorController.java` | `PUT /operator/{id}/disable` | 可禁用/启用**任意 Operator** |
| `OperatorController.java` | `PUT /operator/{id}/enable` | 同上 |
| `OperatorController.java` | `PUT /operator/{id}/reset-password` | 可重置**任意 Operator**密码 |
| `CheckinController.java` | `POST /operator/{activityId}/checkin/manual` | 可代替**任何人**手工签到 |
| `CheckinController.java` | `GET /operator/{activityId}/checkin-stats` | 可查看任意活动签到统计 |
**修复方案:**
```java
private String extractOperatorId(String authHeader) {
String token = extractToken(authHeader);
return jwtTokenService.getUserIdFromToken(token); // 解析 token 获取真实 operatorId
}
```
---
### 【C-02】Operator 权限 Filter 完全旁路
**文件路径:** `backend/src/main/java/com/danyang/match/platform/shared/filter/PermissionFilter.java`
**漏洞行号:** 第 57 行
**漏洞代码:**
```java
// TODO: Phase 02 后续完善权限校验逻辑
// 目前简化:只要通过 JWT 认证即允许访问 Operator 接口
chain.doFilter(request, response); // 无条件放行
```
**根因:** `PermissionFilter` 中权限校验逻辑为 TODO 状态,注释明确说明"只要通过 JWT 认证即允许访问",完全跳过了权限码与 Operator 权限列表的比对。
**影响:** 任何持有 Operator JWT 的攻击者,可访问**全部 Operator 接口**,包括创建/删除 Operator 账号、修改任意用户账号状态、查看所有审核任务和举报记录、访问敏感运营数据。
**修复方案:** 实现完整权限校验逻辑,从请求路径解析所需权限码,与数据库中 Operator 的权限列表比对。参考 Phase 02 规范中的 `permission.manage`、`message.content.review` 等权限定义。
---
### 【C-03】WebSocket 连接未验证 Token
**文件路径:** `backend/src/main/java/com/danyang/match/platform/messaging/websocket/ChatWebSocketHandler.java`
**漏洞行号:** `afterConnectionEstablished` 方法
**漏洞代码:**
```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 = parts[1];
if ("token".equals(parts[0])) token = parts[1];
}
}
}
if (userId == null || token == null) {
session.close(CloseStatus.NOT_ACCEPTABLE);
return;
}
// TODO: Phase 09 验证Token有效性 ← Token 未被验证
sessionUserMap.put(session.getId(), userId); // 直接信任 userId
}
```
**根因:** 从 URL query string 获取 `userId` 和 `token` 后,`token` 未传入 `jwtTokenService.validateToken()` 验证,`userId` 直接从客户端可控参数写入 session 映射。
**影响:**
1. 攻击者可假冒任意用户建立 WebSocket 连接
2. 接收该用户的所有实时消息推送(聊天内容)
3. 冒充该用户发送 WebSocket 消息
**攻击示例:**
```
wss://target.com/ws/chat?userId=VICTIM_UUID&token=fake_or_any_token
```
**修复方案:**
```java
// 在 afterConnectionEstablished 中验证 token
if (!jwtTokenService.validateToken(token)) {
session.close(CloseStatus.NOT_ACCEPTABLE);
return;
}
String verifiedUserId = jwtTokenService.getUserIdFromToken(token);
if (!verifiedUserId.equals(userId)) {
session.close(CloseStatus.NOT_ACCEPTABLE);
return;
}
```
---
### 【C-04】JWT Access Token 密钥硬编码 + 可预测
**文件路径:** `backend/src/main/resources/application.yml`
**漏洞配置:**
```yaml
jwt:
access-token-secret: ${JWT_ACCESS_SECRET:changeme-access-secret-change-in-production}
refresh-token-secret: ${JWT_REFRESH_SECRET:changeme-refresh-secret-change-in-production}
```
**`infrastructure/docker-compose.yml`(dev 环境):**
```yaml
JWT_ACCESS_SECRET: dev-access-secret-do-not-use-in-production
JWT_REFRESH_SECRET: dev-refresh-secret-do-not-use-in-production
```
**根因:**
1. dev 和 staging 环境的 JWT 密钥是公开可预测的字符串
2. `application.yml` 中默认值 `changeme-...` 如果生产环境 `.env` 未覆盖则直接暴露
3. 攻击者获取任意一个 JWT 后,可基于已知密钥伪造任意用户或 Operator 的 token
**修复方案:**
生成符合 JJWT 要求的密钥(HS512 需要至少 512 bits = 64 bytes):
```bash
# 生成 80 字节 base64url 编码的强密钥(107 字符)
python3 -c "import secrets, base64; print(base64.urlsafe_b64encode(secrets.token_bytes(80)).rstrip(b'=').decode())"
```
生产环境 `.env` 必须配置:
```bash
JWT_ACCESS_SECRET=<生成的强密钥,107字符base64url>
JWT_REFRESH_SECRET=<另一个独立强密钥>
```
---
## 🟠 高危漏洞(High)
---
### 【H-01】任意用户公开资料 IDOR — 无需登录可枚举
**文件路径:** `backend/src/main/java/com/danyang/match/platform/profile/controller/ProfileController.java`
**漏洞行号:** 第 60 行
**漏洞代码:**
```java
@GetMapping("/{userId}")
public ApiResponse<PublicUserProfileDTO> getPublicProfile(@PathVariable String userId) {
// ❌ 没有任何 Authorization header 校验
PublicUserProfileDTO profile = userProfileService.getPublicProfile(userId);
if (profile == null) {
return ApiResponse.error(..., "用户不存在");
}
return ApiResponse.ok(profile);
}
```
**根因:** `ProfileController.getPublicProfile` 未校验 Authorization header,未登录用户可直接调用 `/api/v1/profile/{userId}` 获取他人公开资料。
**修复方案:**
```java
@GetMapping("/{userId}")
public ApiResponse<PublicUserProfileDTO> getPublicProfile(
@RequestHeader(value = "Authorization", required = false) String authHeader,
@PathVariable String userId) {
if (authHeader == null) {
return ApiResponse.error(ApiResponseCode.UNAUTHORIZED, "请先登录");
}
String currentUserId = extractUserId(authHeader);
PublicUserProfileDTO profile = userProfileService.getPublicProfile(currentUserId, userId);
// 在 UserProfileService 中检查 currentUserId 是否有权查看 targetUserId 的资料
}
```
---
### 【H-02】CORS 配置允许任意来源
**文件路径:** `backend/src/main/java/com/danyang/match/platform/shared/config/CorsConfig.java`
**漏洞代码:**
```java
config.addAllowedOriginPattern("*"); // 允许所有来源
config.setAllowCredentials(true);
```
**修复方案:** 限制为已知域名,或从环境变量读取:
```java
config.addAllowedOriginPattern(System.getenv("CORS_ALLOWED_ORIGINS"));
```
---
### 【H-03】核心写操作无幂等 Key
涉及接口:`POST /api/v1/relations/{targetUserId}/like`、`POST /api/v1/relations/{targetUserId}/block`、`POST /api/v1/messages/conversations/{id}/send`。
在核心写操作接口中读取 `Idempotency-Key` header,使用 Redis 存储(key = `idempotency:{userId}:{idempotencyKey}`,TTL = 24h)实现防重放。
---
### 【H-04】Refresh Token 泄露后无法完全撤销
**文件路径:** `backend/src/main/java/com/danyang/match/platform/auth/service/AuthService.java`
Redis 中存储的 refreshToken 只用于"是否颁发过",但 JWT 本身即使被删除也能在剩余有效期内(14天)被使用。
**修复方案:** 在 Redis blacklist 中标记已使用的 refreshToken jti,或在 accessToken 中加入 `jti`(JWT ID)字段并在 Redis 中维护 blacklist。
---
### 【H-05】操作审计日志无有效操作者身份
**文件路径:** `CheckinService.java`、`OperatorAuthService.java`
由于 C-01 漏洞,`staffOperatorId` 传入的是无意义的字符串,审计日志无法用于事件溯源。修复 C-01 后自然解决。
---
## 🟡 中危漏洞(Medium)
### 【M-01】JWT Access Token 无 Blacklist
`validateToken()` 只验证签名和时间戳,不检查 Redis blacklist。登出后旧 token 在过期前(2小时)仍可使用。
### 【M-02】文件上传安全检查未实现
`PhotoController.java` 和 `ChatController.java` 中 Phase 10 TODO,`upload-token` 返回 placeholder URL,MinIO 存储配置暴露在客户端。
### 【M-03】缺少安全响应头
`nginx.conf` 缺少 `X-Content-Type-Options`、`X-Frame-Options`、`Content-Security-Policy`、`Strict-Transport-Security`、`Referrer-Policy`。
### 【M-04】AES 加密密钥长度不足
`infrastructure/docker-compose.yml` 中 `ENCRYPTION_KEY: 0123456789abcdef0123456789abcdef` 只有 16 bytes,AES-256 需要 32 bytes。
### 【M-05】无登录失败账户锁定
`OperatorAuthService.java` 中暴力猜解密码无防护,建议 5 次失败后锁定账户。
### 【M-06】Redis 无密码认证
`docker-compose.yml` 中 Redis 未配置密码。
---
## ✅ 设计亮点(未被攻破)
| 方面 | 状态 |
|------|------|
| 密码加密(BCrypt) | ✅ |
| 敏感字段 AES-256-GCM + 随机 IV | ✅ |
| 身份证加密 + Hash 查重 | ✅ |
| 聊天 Mutual Match 校验 | ✅ |
| Block 优先于所有关系操作 | ✅ |
| 数据库唯一约束防重复 | ✅ |
| 签到 Token HMAC + Redis 防重放 | ✅ |
| SQL 注入防护(MyBatis-Plus) | ✅ |
| 统一错误响应格式 | ✅ |
---
## 🔧 修复优先级
### P0 — 必须立即修复(上线前)
| 编号 | 修复内容 | 涉及文件 |
|------|---------|---------|
| C-01 | 修复 `extractOperatorId()` 返回真实 operatorId | `CheckinController.java`、`AccountOperatorController.java`、`OperatorController.java` |
| C-02 | 实现 `PermissionFilter` 权限校验逻辑 | `PermissionFilter.java` |
| C-03 | WebSocket 连接时验证 Token | `ChatWebSocketHandler.java` |
| C-04 | 生产环境配置随机强 JWT 密钥 | `.env`、`docker-compose.prod.yml` |
### P1 — 上线前修复
| 编号 | 修复内容 | 涉及文件 |
|------|---------|---------|
| H-01 | `getPublicProfile` 加登录校验和关系校验 | `ProfileController.java`、`UserProfileService.java` |
| H-02 | CORS 配置限制为已知域名 | `CorsConfig.java` |
| H-04 | refreshToken 加入 Redis blacklist | `AuthService.java`、`JwtTokenService.java` |
| M-01 | accessToken 加入 Redis blacklist | `JwtTokenService.java` |
| M-04 | 修正 ENCRYPTION_KEY 为 32 bytes | `.env`、各环境 compose |
| M-05 | 登录失败锁定(5次) | `OperatorAuthService.java` |
### P2 — 上线后迭代
| 编号 | 修复内容 |
|------|---------|
| H-03 | 关键写操作添加 Idempotency-Key |
| M-02 | 文件上传安全检查(Content-Type、Magic Number、EXIF) |
| M-03 | Nginx 添加安全响应头 |
| M-06 | Redis 密码配置 |
| L-01 | Actuator 端点 IP 白名单 |
| L-02 | 日志脱敏审计 |
---
## 📁 漏洞定位索引
| 文件路径 | 漏洞 |
|---------|------|
| `activity/controller/CheckinController.java:107` | C-01(extractOperatorId 返回 token 字符串) |
| `account/controller/AccountOperatorController.java` | C-01(受影响) |
| `operator/controller/OperatorController.java` | C-01(受影响) |
| `shared/filter/PermissionFilter.java:57` | C-02(TODO 未实现) |
| `messaging/websocket/ChatWebSocketHandler.java:51` | C-03(TODO 未验证 token) |
| `profile/controller/ProfileController.java:60` | H-01(无需认证) |
| `shared/config/CorsConfig.java:16` | H-02(`*` 通配符) |
| `auth/service/AuthService.java:64` | H-04(refreshToken 可在 Redis 删除后使用) |
| `auth/token/JwtTokenService.java` | M-01(无 blacklist) |
| `photo/controller/PhotoController.java` | M-02(TODO) |
| `infrastructure/nginx.conf` | M-03(缺安全头) |
| `infrastructure/docker-compose.yml` | C-04, M-04, M-06 |