## 背景 本次安全审计使用子代理并行覆盖了后端/API、前端/插件、依赖/供应链与部署配置。当前决定暂不处理以下两类架构性问题: - 插件安装边界过宽、外部插件来源/签名/默认启用策略 - 插件无 sandbox、插件同源执行模型 本 issue 聚焦修复其余安全问题,优先收住可远程利用、凭据泄露、XSS、SSRF、路径穿越、默认部署暴露面和供应链可复现性。 ## 需要修复的问题 ### P0:补安全回归测试 先补测试护栏,避免修复后回退。 - 新增 `backend/tests/test_security.py` - 覆盖未认证访问 `/api/models` 返回 401 - 覆盖 `/api/models` 不返回明文 `api_key` - 覆盖 `/fonts/%2e%2e/%2e%2e/data/chatraw.db` 返回 400/404 - 覆盖 `/api/parse-url` 拒绝 `127.0.0.1`、`localhost`、`169.254.169.254`、`192.168.x.x` - 覆盖 `/api/proxy/request` 拒绝内网、IPv6 loopback、DNS 解析到私网的 host - 覆盖 `renderMarkdown('<img src=x onerror=alert(1)>')` 输出不含 `onerror` ### P1:认证、CORS、API Key 泄露 目标:默认本机安全,局域网/公网部署必须显式配置。 - 在 `backend/main.py` 增加配置: - `CHATRAW_AUTH_TOKEN` - `AUTH_ENABLED=true` - `BIND_HOST=127.0.0.1` - 增加 `AuthMiddleware` - 默认保护 `/api/*` - 放行 `/health`、`/ready`、静态资源 - 支持 `Authorization: Bearer <token>` - 修改启动绑定: - `uvicorn.run(app, host=os.environ.get("BIND_HOST", "127.0.0.1"), ...)` - 修改 CORS 默认值: - 不再默认 `CORS_ORIGINS="*"` - 默认只允许 `http://127.0.0.1:51111` 和 `http://localhost:51111`,或要求部署方显式配置 - 修改 `/api/models` - 默认只返回 `api_key_set: true/false` 或 masked key - 不返回明文 `api_key` - `/api/models` POST 保存 key 和 `/api/models/verify` 必须通过认证 ### P2:修复 `/fonts/{path}` 路径穿越 目标:任何编码形式的 `..` 都不能逃出字体目录。 - 优先删除手写 `/fonts/{path:path}` 路由,改用: - `app.mount("/fonts", StaticFiles(directory="static/fonts"), name="fonts")` - 如果保留手写路由: - 使用 `Path("static/fonts").resolve()` 作为根目录 - 对目标路径执行 `resolve()` - 强制 `target.relative_to(font_root)` 成功 - 拒绝 `..`、绝对路径和非必要字体扩展 已验证风险:`/fonts/%2e%2e/%2e%2e/data/chatraw.db` 在当前实现下可返回 200。 ### P3:Markdown XSS 和导出 XSS 目标:所有 Markdown 渲染统一净化。 - 在前端引入本地 vendored `DOMPurify` 或等价白名单净化器 - 修改 `backend/static/app.js` 的 `renderMarkdown(content)` - `marked.parse(content)` 的输出必须经过 sanitizer - 或禁用 raw HTML,仅保留安全 Markdown 渲染 - 同步重建 `backend/static/app.min.js` - 修改 `Plugins/Plugin_market/enhanced-export/main.js` - HTML/PDF 导出也走同一套 sanitizer - 不允许把未净化 `marked.parse()` 结果直接写入 `innerHTML` - 修复插件错误字符串拼 `innerHTML`: - `Plugins/Plugin_market/bocha-search/main.js` - `Plugins/Plugin_market/tavily-search/main.js` - `Plugins/Plugin_market/web-parser/main.js` - 变量内容用 `textContent` 或 `escapeHtml()` ### P4:统一 SSRF Guard 目标:所有服务端出站 URL 走同一套校验。 - 在 `backend/main.py` 新增统一 helper: - `validate_external_url(url, allow_http=False)` - `resolve_hostname(hostname)` - `is_blocked_ip(ip)` - 阻断: - loopback:`127.0.0.0/8`、`::1` - private:`10.0.0.0/8`、`172.16.0.0/12`、`192.168.0.0/16`、`fc00::/7` - link-local:`169.254.0.0/16`、`fe80::/10` - metadata:`169.254.169.254` - multicast、reserved、unspecified 地址 - 应用到: - `/api/fetch-raw-url` - `/api/parse-url` - `/api/proxy/request` - `/api/proxy/upload` - `/api/models/verify` - 禁止 redirect,或每次 redirect 后重新校验目标 URL ### P5:远程 CDN 脚本、SRI、CSP 目标:主应用自身不再依赖无 SRI 的远程脚本。 - 把以下依赖 vendoring 到 `backend/static/vendor/` - `marked` - `Alpine` - `@alpinejs/collapse` - `highlight.js` - 修改 `backend/static/index.html` 中的远程脚本引用为本地路径 - 修改 `backend/static/app.js` 中 highlight.js lazy load 为本地路径 - 增加 CSP header,初始建议: - `default-src 'self'` - `script-src 'self'` - `style-src 'self' 'unsafe-inline'` - `img-src 'self' data: blob:` - `font-src 'self'` - `connect-src 'self' https:` - `frame-ancestors 'self'` 说明:本 issue 不处理第三方插件外部依赖的信任模型,但主应用自身应先移除 CDN 运行时依赖。 ### P6:Docker 默认安全化 目标:默认部署不把服务暴露到整个 host 网络,并降低容器内权限。 - `Dockerfile` - 固定 Python patch 版本或 digest - 创建非 root 用户 `chatraw` - `/app/data` 归属该用户 - 使用 `USER chatraw` - `docker-compose.yml` - 默认移除 `network_mode: host` - 改为 `ports: ["127.0.0.1:51111:51111"]` - 如确需 LAN/host 网络访问,另写 `docker-compose.host.yml` 或 profile - 增加 `security_opt: ["no-new-privileges:true"]` - 增加 `cap_drop: ["ALL"]` ### P7:依赖可复现性与 CI 安全扫描 目标:同一 commit 构建出相同依赖图,CI 文案与真实检查一致。 - Python: - 生成 `backend/requirements.lock` - Docker/CI 使用 lock 安装 - 后续可进一步引入 hash pin - npm: - 从 `.gitignore` 删除 `package-lock.json` - 提交 `package-lock.json` - CI 使用 `npm ci` - CI: - 补齐真实安全扫描,或删掉 PR 模板中的空承诺 - 建议至少加入: - CodeQL for Python/JavaScript - Trivy filesystem/container scan - pip-audit - npm audit 或 GitHub dependency review ## 推荐落地顺序 1. P0:安全回归测试 2. P1:认证、CORS、API Key 脱敏 3. P2:字体路径穿越 4. P4:SSRF guard 5. P3:Markdown XSS 6. P6:Docker 默认安全 7. P5:vendoring/CSP 8. P7:依赖和 CI ## 验收标准 - 默认启动只监听 `127.0.0.1` - 未认证请求无法访问受保护 `/api/*` - `/api/models` 不返回明文 `api_key` - 编码路径穿越无法读取 `data/chatraw.db` - 内网、metadata、loopback URL 均被服务端出站请求拒绝 - 聊天消息和导出 HTML/PDF 中的危险 HTML 不执行 - 主应用运行时不依赖无 SRI 的 CDN 脚本 - 默认 compose 不使用 host network - 依赖安装有锁定文件,CI 与 PR 模板描述一致
背景
本次安全审计使用子代理并行覆盖了后端/API、前端/插件、依赖/供应链与部署配置。当前决定暂不处理以下两类架构性问题:
本 issue 聚焦修复其余安全问题,优先收住可远程利用、凭据泄露、XSS、SSRF、路径穿越、默认部署暴露面和供应链可复现性。
需要修复的问题
P0:补安全回归测试
先补测试护栏,避免修复后回退。
backend/tests/test_security.py/api/models返回 401/api/models不返回明文api_key/fonts/%2e%2e/%2e%2e/data/chatraw.db返回 400/404/api/parse-url拒绝127.0.0.1、localhost、169.254.169.254、192.168.x.x/api/proxy/request拒绝内网、IPv6 loopback、DNS 解析到私网的 hostrenderMarkdown('<img src=x onerror=alert(1)>')输出不含onerrorP1:认证、CORS、API Key 泄露
目标:默认本机安全,局域网/公网部署必须显式配置。
backend/main.py增加配置:CHATRAW_AUTH_TOKENAUTH_ENABLED=trueBIND_HOST=127.0.0.1AuthMiddleware/api/*/health、/ready、静态资源Authorization: Bearer <token>uvicorn.run(app, host=os.environ.get("BIND_HOST", "127.0.0.1"), ...)CORS_ORIGINS="*"http://127.0.0.1:51111和http://localhost:51111,或要求部署方显式配置/api/modelsapi_key_set: true/false或 masked keyapi_key/api/modelsPOST 保存 key 和/api/models/verify必须通过认证P2:修复
/fonts/{path}路径穿越目标:任何编码形式的
..都不能逃出字体目录。/fonts/{path:path}路由,改用:app.mount("/fonts", StaticFiles(directory="static/fonts"), name="fonts")Path("static/fonts").resolve()作为根目录resolve()target.relative_to(font_root)成功..、绝对路径和非必要字体扩展已验证风险:
/fonts/%2e%2e/%2e%2e/data/chatraw.db在当前实现下可返回 200。P3:Markdown XSS 和导出 XSS
目标:所有 Markdown 渲染统一净化。
DOMPurify或等价白名单净化器backend/static/app.js的renderMarkdown(content)marked.parse(content)的输出必须经过 sanitizerbackend/static/app.min.jsPlugins/Plugin_market/enhanced-export/main.jsmarked.parse()结果直接写入innerHTMLinnerHTML:Plugins/Plugin_market/bocha-search/main.jsPlugins/Plugin_market/tavily-search/main.jsPlugins/Plugin_market/web-parser/main.jstextContent或escapeHtml()P4:统一 SSRF Guard
目标:所有服务端出站 URL 走同一套校验。
backend/main.py新增统一 helper:validate_external_url(url, allow_http=False)resolve_hostname(hostname)is_blocked_ip(ip)127.0.0.0/8、::110.0.0.0/8、172.16.0.0/12、192.168.0.0/16、fc00::/7169.254.0.0/16、fe80::/10169.254.169.254/api/fetch-raw-url/api/parse-url/api/proxy/request/api/proxy/upload/api/models/verifyP5:远程 CDN 脚本、SRI、CSP
目标:主应用自身不再依赖无 SRI 的远程脚本。
backend/static/vendor/markedAlpine@alpinejs/collapsehighlight.jsbackend/static/index.html中的远程脚本引用为本地路径backend/static/app.js中 highlight.js lazy load 为本地路径default-src 'self'script-src 'self'style-src 'self' 'unsafe-inline'img-src 'self' data: blob:font-src 'self'connect-src 'self' https:frame-ancestors 'self'说明:本 issue 不处理第三方插件外部依赖的信任模型,但主应用自身应先移除 CDN 运行时依赖。
P6:Docker 默认安全化
目标:默认部署不把服务暴露到整个 host 网络,并降低容器内权限。
Dockerfilechatraw/app/data归属该用户USER chatrawdocker-compose.ymlnetwork_mode: hostports: ["127.0.0.1:51111:51111"]docker-compose.host.yml或 profilesecurity_opt: ["no-new-privileges:true"]cap_drop: ["ALL"]P7:依赖可复现性与 CI 安全扫描
目标:同一 commit 构建出相同依赖图,CI 文案与真实检查一致。
backend/requirements.lock.gitignore删除package-lock.jsonpackage-lock.jsonnpm ci推荐落地顺序
验收标准
127.0.0.1/api/*/api/models不返回明文api_keydata/chatraw.db