Files
Cyrene/docs/debug/2026-05-20-round5-security-boundary.md
T

349 lines
13 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.
# 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
**下一轮计划**: 修复上述安全问题