# zm-rag 全项目审查报告（Copilot 独立审查）

日期：2026-03-16

审查范围：本次以当前运行时主链路为重点，覆盖 backend/app、frontend/src、converter/src/main，以及与暴露面直接相关的 docker 配置。重点关注认证鉴权、测试/Mock 暴露、Webhook 边界、前端渲染安全、配置安全和契约一致性。

## 结论摘要

当前项目的主要问题不在“能否跑起来”，而在“边界是否收住”。

- 前端生产构建可通过：在 frontend 目录执行 `npx vite build` 成功，只有大 chunk 警告。
- 定向后端测试未全绿：执行 `python -m pytest tests/test_api_admin.py tests/test_api_ingest.py tests/test_acl_boundary.py -q` 时，`tests/test_api_ingest.py` 有 3 个失败，另有 40 个通过。
- 当前最需要优先处理的是 4 类边界问题：Mock 测试接口暴露、管理接口缺失 RBAC、Webhook 未认证、前端 HTML 注入链路。

## 关键发现

### 1. Critical: Mock 测试接口在正式路由中永久暴露，且包含未认证的发令牌、删文档、查任务、查文档能力

证据：

- `backend/app/api/v1/router.py:32` 无条件注册 `mock_router`。
- `backend/app/api/v1/mock.py:167` 提供 `/mock/token`，可直接签发业务 JWT。
- `backend/app/api/v1/mock.py:215` 提供 `/mock/login`，使用内置测试账号登录。
- `backend/app/api/v1/mock.py:263`、`280`、`341`、`383`、`416` 分别暴露测试用户、文档列表、任务状态、删除文档、系统状态接口，且这些处理器没有认证依赖。
- `frontend/src/router/index.ts:80` 永久注册了 `/mock` 路由；`frontend/src/App.vue:97` 只是开发模式下隐藏菜单，不是访问控制。

影响：

- 任何能访问后端的人都可以自助生成系统可接受的 JWT，再去访问所有受 JWT 保护的接口。
- `/mock/doc/{doc_id}` 允许未认证删除文档，已经越过“测试便利性”范围，属于直接的破坏性能力暴露。
- 前端虽然默认不在生产菜单显示 Mock 页面，但知道路径的人仍可直接访问 `/mock`。

建议：

- 默认不注册 mock 路由，只有显式开启的开发/测试环境才挂载。
- 即使在测试环境，也不要让 `/mock/token` 使用生产 JWT 密钥签发真实业务令牌。
- 所有带副作用的 mock 接口至少增加二次保护，例如独立密钥、内网限制或 Basic Auth。

### 2. High: 后端管理接口只有“已登录”检查，没有管理员/运维权限校验

证据：

- `backend/app/api/deps.py:137` 只负责验签并解析 JWT，`backend/app/api/deps.py:162` 只是把 `role_ids` 放进 `UserContext`。
- `backend/app/api/v1/admin.py:29`、`108`、`168` 等所有管理接口都只依赖 `get_current_user`。
- `backend/app/api/v1/admin_graph.py:72`、`82`、`116`、`126` 等图谱管理接口也只依赖 `get_current_user`。
- 在 `backend/app/api/v1/admin.py` 和 `backend/app/api/v1/admin_graph.py` 中未发现对 `role_ids`、`user.role` 或管理员角色常量的校验。

影响：

- 任意普通登录用户都可能访问文档删除、图谱重建、类型定义管理、实体合并等本应属于后台管理员的能力。
- 结合前述 Mock 发令牌问题，后台能力基本等于对外开放。

建议：

- 增加 `require_admin_user` 或基于权限点的依赖，并在所有 `/admin`、`/admin/graph` 路由统一使用。
- 不要把“前端菜单是否显示”当作权限控制。

### 3. High: Webhook 入库与 ACL 更新端点未做来源认证，可被任意调用

证据：

- `backend/app/api/v1/ingest.py:263` 定义 `/ingest/webhook/document`，没有 `Depends(get_current_user)`、签名校验或共享密钥校验。
- `backend/app/api/v1/ingest.py:382` 定义 `/ingest/webhook/permission`，同样没有来源认证。

影响：

- 外部请求可以伪造文档入库、制造大量转换/Embedding/索引任务，形成资源消耗。
- 外部请求可以伪造 ACL 更新，直接改变文档可见范围。

建议：

- 为 Webhook 增加 HMAC 签名或独立的服务间 JWT。
- 配合 Nginx/网关做源地址白名单和速率限制。
- 为关键更新增加幂等键和审计字段。

### 4. High: Converter 服务对外开放未认证文件上传与日志查询，且 docker 默认映射宿主机端口

证据：

- `converter/src/main/java/com/zmrag/converter/controller/ConvertController.java:19`、`20` 暴露 `/api/convert` 控制器。
- `converter/src/main/java/com/zmrag/converter/controller/ConvertController.java:38` 暴露未认证的文件转换 POST 接口。
- `converter/src/main/java/com/zmrag/converter/controller/ConvertController.java:84` 暴露未认证的日志查询 GET 接口。
- `docker/docker-compose.yml:124` 将转换服务映射为 `18800:18800`，不是仅容器内访问。

影响：

- 只要宿主机端口可达，就可以直接向转换服务提交文件，形成 CPU、内存、磁盘占用风险。
- 转换日志接口可能暴露文件名、状态和错误信息。

建议：

- 如果该服务仅供 backend 内部调用，应取消宿主机端口映射，仅保留容器内网络访问。
- 若必须对外开放，至少增加认证和上传大小/频率限制。

### 5. High: 前端仍有未清洗的 `v-html` 渲染链路，且 JWT 存在 localStorage，可形成令牌窃取链

证据：

- `frontend/src/views/SearchView.vue:317` 将搜索结果 `doc.highlights` 原样赋给 `previewHighlights`。
- `frontend/src/components/DocumentQuickPreview.vue:48` 使用 `v-html` 原样渲染 highlight。
- `frontend/src/views/MockOA.vue:677` 同样使用 `v-html` 原样渲染搜索高亮。
- `frontend/src/stores/user.ts:10` 从 `localStorage` 读取 token，`frontend/src/stores/user.ts:45` 删除 token。
- `frontend/src/api/request.ts:22` 每次请求都从 `localStorage` 取 token 注入 Authorization。

影响：

- 如果索引内容或高亮片段里带入恶意 HTML/事件属性，这两个渲染点会直接执行。
- 一旦前端发生 XSS，localStorage 中的 JWT 很容易被读取并外传。

建议：

- 所有高亮渲染统一复用 `frontend/src/utils/sanitize.ts` 的清洗逻辑，至少只允许 `<em>`。
- 中长期应评估把访问令牌迁移到 httpOnly cookie，降低 XSS 后果。

### 6. Medium: 限流中间件信任未验签 JWT payload 里的 `sub`，可被轻易规避或污染配额

证据：

- `backend/app/middleware/rate_limit.py:71`、`72` 直接 base64 解码 JWT payload。
- `backend/app/middleware/rate_limit.py:74` 直接把 payload 中的 `sub` 作为限流 key 的用户标识。

影响：

- 攻击者可以伪造任意 `sub` 来分散请求，从而规避基于用户的限流。
- 也可以伪造他人的 `sub`，把别人的桶打满，制造定向限流。

建议：

- 不要在限流层手工解析未验签 token。
- 若必须按用户限流，应复用认证后的用户上下文；否则按 IP 或原始 token 哈希限流。

### 7. Medium: 入库触发接口的契约与测试集已经脱节，当前定向测试直接失败

证据：

- `backend/app/api/schemas/ingest.py:18` 当前模型要求 `file_path`。
- `backend/app/api/v1/ingest.py:184` 明确返回 `file_path is required`。
- `backend/app/api/v1/ingest.py:195` 还要求路径必须位于存储目录内。
- `backend/tests/test_api_ingest.py:21`、`31`、`47` 仍然向 `/ingest/trigger` 发送 `pdf_path`。
- 本次执行 `python -m pytest tests/test_api_admin.py tests/test_api_ingest.py tests/test_acl_boundary.py -q`，`tests/test_api_ingest.py` 中有 3 个失败，均围绕这个契约变化。

影响：

- 当前 ingest 相关测试已经不能真实反映生产接口行为。
- 接口安全加固是对的，但若测试、文档、调用方不一起更新，后续很容易把“预期变更”误判成“功能损坏”，或反之。

建议：

- 统一决定是否保留 `pdf_path` 兼容层；如果不保留，就同步更新测试、文档和所有非前端调用方。
- 将“路径必须在 storage 目录内”写入接口文档，而不是只体现在运行时报错里。

### 8. Medium: 配置仍保留危险默认值，缺少生产环境 fail-fast

证据：

- `backend/app/config.py:45` 默认 `es_verify_certs = False`。
- `backend/app/config.py:52` 默认 Neo4j 密码是固定值。
- `backend/app/config.py:100` 默认 JWT 密钥是固定字符串。

影响：

- 一旦部署环境遗漏变量注入，系统会以弱配置直接启动，而不是在启动阶段拒绝运行。
- 这类问题通常不是开发环境 bug，而是生产事故前的隐患。

建议：

- 在非 debug 环境下，对默认密钥、默认密码、关闭 TLS 校验等情况直接启动失败。
- 将“开发默认值”与“生产必填值”拆分，不要共用同一套默认配置。

## 本次未计入问题的项

以下问题我没有再计入当前发现，因为在现代码里已经看到修复或明显改善：

- `frontend/src/components/ResultCard.vue:12`、`44` 已对标题和高亮做清洗，不再是原始 `v-html` 直出。
- `frontend/src/utils/markdown.ts:10` 已关闭 Markdown 原始 HTML（`html: false`），因此“MarkdownRenderer 可直接执行任意原始 HTML”的旧结论不再成立。
- `backend/app/main.py:149`、`151`、`152` 的 CORS 配置已收紧到明确 origin、method、header，不再是典型的全放开配置。
- `frontend/src/views/admin/GraphManager.vue:34`、`74`、`76` 已在父组件挂载时主动加载实体/关系类型，之前“首次进入非类型页下拉为空”的回归在当前代码中已修复。

## 修复优先级建议

建议按下面顺序处理：

1. 立即下线或强保护 Mock 路由与未认证 destructive mock 接口。
2. 给 `/admin`、`/admin/graph`、converter、webhook 加上真正的鉴权边界。
3. 清理所有未清洗的 `v-html` 渲染，并评估 token 存储策略。
4. 修复限流身份来源与 ingest 契约/测试漂移。
5. 增加生产配置 fail-fast，避免默认弱配置直接启动。
