docs: add round 5 debug report - security audit and boundary testing

This commit is contained in:
2026-05-20 14:53:57 +08:00
parent d239b958df
commit 7daa8a9b23
@@ -0,0 +1,348 @@
# Cyrene 第5轮调试报告:安全审计 + 边界条件 + 错误处理
> **日期**2026-05-20 14:39 CST (UTC+8)
> **轮次**:第5轮
> **测试方法**:黑盒渗透测试 (curl + 手动审计)
> **测试范围**Gateway (8080) 全端点
---
## 一、测试概况
| 类别 | 测试项数 | 通过 | 发现问题 |
|------|---------|------|---------|
| 认证绕过 | 9 | 8 | 1 |
| SQL注入 | 6 | 4 | 2 |
| XSS | 5 | 0 | 5 |
| 路径遍历 | 3 | 3 | 0 |
| 权限提升 | 1 | 1 | 0 |
| 速率限制 | 2 | 0 | 2 |
| 边界条件 | 10 | 5 | 5 |
| 错误处理 | 8 | 3 | 5 |
| **合计** | **44** | **24** | **20** |
---
## 二、严重发现 (🔴 Critical)
### 🔴 SEC-001: 注册/登录端点无速率限制
**位置**: [`backend/gateway/internal/router/router.go`](backend/gateway/internal/router/router.go:56-60)
**描述**: 登录 (`/auth/login`) 和注册 (`/auth/register`) 端点属于公开路由组,未应用限流中间件。`RateLimiter` 仅附加在 `protected` 路由组上。
**测试方法**:
```bash
for i in $(seq 1 20); do
curl -s -X POST http://localhost:8080/api/v1/auth/login \
-H 'Content-Type: application/json' \
-d '{"username":"admin","password":"wrong"}'
done
```
**实际结果**: 20次请求全部返回 `401`,无任何 `429 Too Many Requests`
**预期结果**: 应在短时间内(如1秒内超过5次)返回 `429` 限流响应。
**影响**: 攻击者可对登录端点进行暴力破解,无限尝试密码组合。理论上 JWT 过期时间为 720 小时(30天),但暴力破解风险依然存在。
**严重级别**: 🔴 Critical
---
### 🔴 SEC-002: 用户名未做危险字符过滤 (Stored XSS + Log注入)
**位置**: [`backend/gateway/internal/handler/auth_handler.go`](backend/gateway/internal/handler/auth_handler.go:38-45)
**描述**: 注册端点接受包含 SQL 注入 payload、XSS payload 的用户名。虽然数据库查询使用了参数化查询(`$1`)防止了实际注入执行,但恶意字符串被原样存储到数据库,并嵌入到 JWT token 的 `user_id` 字段中。
**测试方法**:
```bash
curl -s -X POST http://localhost:8080/api/v1/auth/register \
-H 'Content-Type: application/json' \
-d '{"username":"test'\''; DROP TABLE users; --","password":"Test@123456",
"email":"sqli@test.com","nickname":"SQLiTest","verify_code":"000000"}'
```
**实际结果**: 用户成功注册,`user_id``user_test'; DROP TABLE users; --`JWT token 中包含此 payload。用户可以使用此用户名正常登录。
**影响**:
1. 日志注入:用户名出现在日志中可能破坏日志格式
2. 存储型 XSS:如果前端任何地方直接渲染 `user_id` 而未转义
3. 数据完整性:数据库中存储了恶意构造的用户名
4. Token 膨胀:JWT 中包含特殊字符
**严重级别**: 🔴 Critical
---
### 🔴 SEC-003: 存储型 XSS — 所有内容端点未对输入做 HTML 转义
**位置**:
- [`backend/gateway/internal/handler/memory_handler.go`](backend/gateway/internal/handler/memory_handler.go)
- [`backend/gateway/internal/handler/knowledge_handler.go`](backend/gateway/internal/handler/knowledge_handler.go)
- [`backend/gateway/internal/handler/reminder_handler.go`](backend/gateway/internal/handler/reminder_handler.go)
**描述**: 记忆、知识库、提醒的标题/内容/便签字段接受并存储 HTML/JavaScript 代码。虽然 JSON 响应中使用了 Unicode 转义(如 `\u003cscript\u003e`),但这依赖 JSON 序列化器的默认行为,后端没有主动做输入验证或输出净化。
**测试方法**:
```bash
# 记忆
curl -X POST /api/v1/memory -H "Authorization: Bearer $TOKEN" \
-d '{"content":"<img src=x onerror=alert(1)>","category":"xss_test"}'
# 知识库
curl -X POST /api/v1/knowledge/bases -H "Authorization: Bearer $TOKEN" \
-d '{"name":"<script>alert(1)</script>","description":"XSS test"}'
# 提醒
curl -X POST /api/v1/reminders -H "Authorization: Bearer $TOKEN" \
-d '{"title":"<b>Test</b>","note":"<script>alert(document.cookie)</script>",...}'
```
**实际结果**: 所有 XSS payload 被原样存储到数据库。JSON 响应中由 Go 的 `encoding/json` 自动转义为 Unicode 形式。
**影响**: 如果前端渲染这些内容时使用 `dangerouslySetInnerHTML` 或未做 HTML 转义,将导致存储型 XSS 攻击。即使后端 JSON 有转义,前端可能自行解析。
**严重级别**: 🔴 Critical (取决于前端实现)
---
### 🔴 SEC-004: 文件名 XSS 过滤过于粗暴且不透明
**位置**: [`backend/gateway/internal/handler/file_handler.go`](backend/gateway/internal/handler/file_handler.go)
**描述**: 文件上传端点对文件名中的 `<script>` 标签做了过滤,但过滤方式是静默删除标签字符,导致用户得到的文件名与预期完全不同。
**测试方法**:
```bash
echo "XSS test" > /tmp/xss_test.txt
curl -X POST /api/v1/files/upload -H "Authorization: Bearer $TOKEN" \
-F "file=@/tmp/xss_test.txt;filename=<script>alert(1)</script>.txt"
```
**实际结果**: 文件名被变成 `script_.txt``<`, `/`, `>` 字符被移除)。原始文件名信息丢失。没有向用户说明文件名被修改的原因。
**影响**:
1. 用户提交 `filename=<script>.txt` → 得到 `script_.txt`,造成困惑
2. 没有告警或说明文件名被清理过
3. 过滤逻辑不透明
**严重级别**: 🔴 High (功能性 + 安全)
---
### 🔴 SEC-005: 超大内容无大小限制
**位置**:
- [`backend/gateway/internal/handler/memory_handler.go`](backend/gateway/internal/handler/memory_handler.go)
- [`backend/gateway/internal/handler/knowledge_handler.go`](backend/gateway/internal/handler/knowledge_handler.go)
**描述**: 记忆和知识库的 content 字段没有大小限制。测试中成功存储了 100,000 字符的内容。
**测试方法**:
```bash
curl -X POST /api/v1/memory -H "Authorization: Bearer $TOKEN" \
-d "{\"content\":\"$(python3 -c "print('X'*100000)")\",\"category\":\"big\"}"
```
**实际结果**: HTTP 200,100KB 内容成功存储。查询时返回完整内容(响应体超过 99KB)。
**影响**:
1. 数据库存储膨胀
2. API 响应过大可能导致前端卡顿或崩溃
3. 可被用于 DoS 攻击
4. 带宽浪费
**严重级别**: 🔴 Medium-High
---
## 三、中等发现 (🟡 Medium)
### 🟡 SEC-006: JWT Secret 使用弱默认值
**位置**: [`backend/gateway/internal/config/config.go`](backend/gateway/internal/config/config.go:92)
**描述**: JWT 签名密钥默认为 `change-me-in-production`,此值在源代码中明文可见。虽然生产环境应通过环境变量覆盖,但开发者可能忘记设置。
**当前值**: `getEnv("JWT_SECRET", "change-me-in-production")`
**影响**: 如果生产部署未覆盖此值,攻击者可伪造任意用户的 JWT token。
**严重级别**: 🟡 Medium
---
### 🟡 SEC-007: 管理员权限判断依赖用户名前缀
**位置**: [`backend/gateway/internal/middleware/auth.go`](backend/gateway/internal/middleware/auth.go:45)
**描述**: 管理员权限通过检查 `user_id` 是否以 `admin_` 前缀开头来判断,而非使用数据库中的 `is_admin` 字段。
```go
c.Set(IsAdminKey, strings.HasPrefix(userID, "admin_"))
```
**影响**: 如果攻击者能注册一个用户名为 `admin_fake` 的账户(如果注册开关开启),其 `user_id` 将变成 `user_admin_fake`(非管理员)。但如果数据库中有用户 `admin_xxx` 且能通过其他方式认证... 不过由于 `user_id = "user_" + username` 的拼接方式,普通用户无法获得 `admin_` 前缀。
**严重级别**: 🟡 Low-Medium (设计缺陷,但实际风险取决于注册是否开放)
---
### 🟡 SEC-008: 负数值分页参数未被拒绝
**位置**: 搜索端点(sessions/messages/search, memory/search
**描述**: `limit=-1``offset=-100` 被静默接受(HTTP 200),虽然实际返回结果正确,但应返回 400 错误。
**测试方法**:
```bash
curl "$BASE/messages/search?q=test&limit=-1&offset=-100" -H "Authorization: Bearer $TOKEN"
# HTTP 200
```
**影响**: 低风险,但不符合健壮 API 设计原则。
**严重级别**: 🟡 Low-Medium
---
### 🟡 SEC-009: 空字节导致 500 内部错误
**位置**: [`backend/gateway/internal/handler/auth_handler.go`](backend/gateway/internal/handler/auth_handler.go:116)
**描述**: 发送包含 `\u0000` 的 JSON 请求体导致服务器返回 `{"error":"服务器内部错误"}` (500),而非 400 参数无效。
**测试方法**:
```bash
curl -X POST /api/v1/auth/login -H 'Content-Type: application/json' \
-d '{"username":"test\u0000user","password":"pass\u0000word"}'
# HTTP 500: {"error":"服务器内部错误"}
```
**影响**: 攻击者可通过发送包含空字节的请求探测服务器内部行为,也可能导致日志膨胀。
**严重级别**: 🟡 Medium
---
### 🟡 SEC-010: 错误 HTTP 方法返回 404 而非 405
**位置**: [`backend/gateway/internal/router/router.go`](backend/gateway/internal/router/router.go)
**描述**: 对端点使用不支持的 HTTP 方法时返回 `404 Not Found` 而非 `405 Method Not Allowed`
| 端点 | 正确方法 | 测试方法 | 实际 | 预期 |
|------|---------|---------|------|------|
| `/health` | GET | PATCH | 404 | 405 |
| `/health` | GET | POST | 404 | 405 |
| `/auth/login` | POST | PUT | 404 | 405 |
**影响**: 误导客户端,不利于 API 可发现性。
**严重级别**: 🟡 Low
---
## 四、低风险发现 (🟢 Low)
### 🟢 SEC-011: 错误消息泄露验证细节
**描述**: 注册失败时的错误消息包含完整的字段验证信息:
```
{"error":"请求参数无效: Key: 'Password' Error:Field validation for 'Password' failed on the 'required' tag\n..."}
```
**影响**: 暴露了后端使用的验证框架(gin binding)和字段名称。
**严重级别**: 🟢 Low
### 🟢 SEC-012: 速率限制仅应用于受保护端点
**描述**: `RateLimiter` 配置为 10 req/s, burst 20,但仅通过 `protected.Use(rateLimiter.Handler())` 应用于需要认证的路由。公开端点(health, login, register)无限制。
**测试**: 15次快速连续请求到受保护端点均返回 200,说明 burst=20 在当前测试中未触发限流。需要更高频率的测试。
**严重级别**: 🟢 Low (限流存在但有盲区)
### 🟢 SEC-013: 超大搜索查询被接受
**描述**: 5000个字符的搜索查询被接受并正常处理(HTTP 200)。
**严重级别**: 🟢 Low
---
## 五、通过的安全检查 ✅
| 测试项 | 结果 | 说明 |
|--------|------|------|
| 无 Token 访问 | ✅ 401 | 所有受保护端点正确拒绝 |
| 空 Authorization 头 | ✅ 401 | 正确处理 |
| 非 Bearer 格式 | ✅ 401 | 格式验证正确 |
| 伪造 Token | ✅ 401 | JWT 签名验证有效 |
| 篡改 Token | ✅ 401 | 签名不匹配被拒绝 |
| 过期 Token | ✅ 401 | 过期检测正常 |
| 管理员权限隔离 | ✅ 403 | 普通用户访问 /admin 被拒绝 |
| 路径遍历 (../) | ✅ 404 | 文件/知识库端点正确阻止 |
| 路径遍历 (URL编码) | ✅ 404 | `%2F` 编码的遍历也被阻止 |
| 并发请求 | ✅ 200x10 | 10个并发请求全部成功,无竞态 |
| 不存在的资源 | ✅ 404 | 正确处理 |
| 健康检查公开 | ✅ 200 | 无需认证即可访问 |
| SQL参数化查询 | ✅ | 所有 DB 查询使用 `$1` 占位符 |
| 文件类型白名单 | ✅ | 仅允许安全类型 |
---
## 六、发现汇总
| ID | 类别 | 标题 | 严重级别 |
|----|------|------|---------|
| SEC-001 | 速率限制 | 登录/注册端点无限流保护 | 🔴 Critical |
| SEC-002 | 输入验证 | 用户名缺少危险字符过滤 | 🔴 Critical |
| SEC-003 | XSS | 内容端点未做HTML转义 | 🔴 Critical |
| SEC-004 | XSS | 文件名过滤不透明 | 🔴 High |
| SEC-005 | 边界条件 | 内容大小无上限 | 🔴 Medium-High |
| SEC-006 | 配置安全 | JWT Secret 弱默认值 | 🟡 Medium |
| SEC-007 | 权限模型 | 管理员判断依赖前缀 | 🟡 Low-Medium |
| SEC-008 | 边界条件 | 负数分页参数被接受 | 🟡 Low-Medium |
| SEC-009 | 错误处理 | 空字节导致500错误 | 🟡 Medium |
| SEC-010 | 错误处理 | 错误方法返回404而非405 | 🟡 Low |
| SEC-011 | 信息泄露 | 错误消息暴露验证细节 | 🟢 Low |
| SEC-012 | 速率限制 | 公开端点无速率限制 | 🟢 Low |
| SEC-013 | 边界条件 | 超大搜索查询被接受 | 🟢 Low |
---
## 七、建议优先级修复顺序
1. **SEC-001** — 为公开端点添加速率限制(最快修复,最高影响)
2. **SEC-002** — 用户名正则校验 + 字符白名单
3. **SEC-003** — 输入净化/HTML实体转义中间件
4. **SEC-005** — 添加请求体大小限制和内容长度校验
5. **SEC-006** — 环境变量强制检查(启动时验证非默认值)
6. **SEC-004** — 改进文件名清理逻辑并通知用户
7. **SEC-009** — 空字节检测 + 400 错误
8. **SEC-010** — Gin 路由添加 `NoMethod` 处理
9. **SEC-007** — 使用数据库 `is_admin` 字段
10. **SEC-008** — 分页参数范围校验
---
## 八、测试环境
- **目标**: `http://localhost:8080`
- **服务状态**: 全部6个微服务运行正常
- Gateway (8080) ✅
- AI-Core (8081) ✅
- IoT-Debug (8083) ✅
- Memory (8091) ✅
- Tool-Engine (8092) ✅
- Voice (8093) ✅
- **数据库**: PostgreSQL (SSH 隧道连接)
- **测试账号**: `user_secaudit_tester` (普通用户)
- **测试时间**: 2026-05-20 14:39 - 14:50 CST
---
**报告生成时间**: 2026-05-20 14:50 CST
**下一轮计划**: 修复上述安全问题