Files
ephron-ren-prd/security-audits/2026-05-16-ephron-ren-security-audit-prd.md

382 lines
11 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.
# ephron.ren 全站安全审计 PRD
- 日期2026-05-16
- 审计方式:源码静态审计 + 浏览器验证 + HTTP 探测 + 安全边界内渗透式验证
- 审计对象auth / blog / canvas / prompt / home / shared
- 代码基线:`ephron_ren/ephron.ren` commit `bca0e08efdd33de85550680496528c40588a7501`
## 一、结论摘要
本轮无法证明“整个站点已经安全”,但已经可以确认:
1. 项目具备一套**基础安全基线**
- 服务端 admin 鉴权
- 权限检查
- CSRF 防护(大部分后台写接口)
- 统一安全头
- 登录限流 / 若干业务限流
- Cookie 基本属性HttpOnly / SameSite / Secure
2. 但本轮也发现了**至少 2 个可确认安全漏洞**,其中 1 个优先级较高:
- `prompt` 存在**匿名 SSRF 探针接口**
- `prompt` 存在**已登录态下缺少 CSRF 的写/调用接口**
3. 此外还有一些**中低风险加固项**
- CSP 允许 `unsafe-inline`,显著削弱 XSS 防线
- logout 设计允许 GET/无 CSRF存在强制登出面
- 若干 prompt 调试/测试能力暴露边界偏宽
因此,当前状态不能下“已确保项目安全”的结论。更准确的说法是:
> **站点不是明显裸奔,有基础防护,但仍存在已确认漏洞,尚未达到可宣称“安全”的状态。**
---
## 二、审计范围
### 覆盖内容
- 认证 / 授权 / 会话 / Cookie
- CSRF
- 路由保护与 admin 边界
- 重定向与登录回跳
- XSS / Markdown / HTML 渲染
- 上传
- Prompt 调试与 provider 调用链
- service token / service API 信任边界
- 安全头 / 限流
- 公开 HTTP 行为与浏览器链路
### 未覆盖或仅部分覆盖
- 登录后真实管理员态的全量人工操作验证
- 大规模自动目录爆破 / 口令喷洒 / 破坏性测试
- 依赖库 CVE 全量 SBOM 扫描
- 生产基础设施层Nginx/WAF/容器/DB/内网 ACL
- 外部云资源 / 内网元数据可达性验证
---
## 三、已确认漏洞
## 漏洞 1Prompt 存在匿名 SSRF 探针接口
### 级别
### 位置
- `prompt/src/routes/api.py:272-336`
- 前端调用入口:`prompt/templates/admin/settings.html:601-611`
### 问题描述
`POST /api/test-connection` 允许调用方提交:
- `provider`
- `base_url`
- `api_key`
- `model`
后端会直接基于传入的 `base_url` 向外发起 HTTP 请求,但该接口代码中**没有任何登录校验、权限校验、CSRF 校验**。
这意味着任意未登录访问者都可以把这个接口当作:
- 服务端网络可达性探针
- SSRF 跳板
- 响应探测器
### 代码证据
`prompt/src/routes/api.py`
- `272`: `@router.post("/test-connection")`
- `273`: `async def test_connection(payload: TestConnectionRequest):`
- 未见 `get_auth_user(...)`
- 未见 `require_admin_role(...)`
- 未见 `verify_csrf_token(...)`
- `281`: `f"{payload.base_url}/v1/messages"`
- `309`: `f"{payload.base_url}/chat/completions"`
### 动态验证证据
未登录直接请求:
```http
POST https://prompt.ephron.ren/api/test-connection
Content-Type: application/json
{
"provider": "openai",
"base_url": "https://example.com",
"api_key": "test",
"model": "gpt-4o-mini"
}
```
返回:
- HTTP 200
- body 含 `HTTP 405: <!doctype html><html ... Example Domain ...>`
说明:
1. 接口对匿名用户开放
2. 后端确实向 `https://example.com/chat/completions` 发起了请求
3. 目标响应片段被带回调用方
### 影响
- 可用于探测服务器对外网络访问能力
- 可作为 SSRF 探针访问内网/控制面/云元数据等目标(实际影响取决于部署网络)
- 可帮助攻击者绘制内网/出口访问面
- 若未来返回信息更多,可能升级为更强的信息泄露
### 判定
**已确认漏洞**
### 修复目标
- 该接口必须至少要求管理员鉴权
- 同时建议加入 CSRF 防护
-`base_url` 做 allowlist 或严格限制为已保存 provider 域名
- 不应把上游原始响应文本直接回显给前端
---
## 漏洞 2Prompt 测试 / 保存示例接口缺少 CSRF 防护
### 级别
### 位置
- `prompt/src/routes/api.py:141-262`
- 前端调用:`prompt/static/js/test-prompt.js:104-108`
### 问题描述
以下两个接口依赖 Cookie 登录态,但没有做 CSRF 校验:
1. `POST /api/prompts/{key}/test`
2. `POST /api/prompts/{key}/test/save-example`
项目其它后台写操作普遍使用 `shared.csrf.verify_csrf_token(...)`,但这两处没有跟进。
### 代码证据
#### 1) 测试接口
`prompt/src/routes/api.py`
- `141`: `@router.post("/prompts/{key}/test")`
- `147`: `token = request.cookies.get("ephron_auth")`
- `148`: `user = get_auth_user(token)`
- 未见 `verify_csrf_token(...)`
- `186-214`: 会调用 `chat_completion(...)`
#### 2) 保存示例接口
`prompt/src/routes/api.py`
- `234`: `@router.post("/prompts/{key}/test/save-example")`
- `240`: `token = request.cookies.get("ephron_auth")`
- `241`: `user = get_auth_user(token)`
- 未见 `verify_csrf_token(...)`
- `252-257`: 调用 `update_prompt(...)`
#### 前端调用方式
`prompt/static/js/test-prompt.js`
- `104`: `fetch(`/api/prompts/${this.promptKey}/test`, {`
- `107`: 仅 `Content-Type: application/json`
- 未附带 CSRF header / token
### 影响
- 已登录用户若访问恶意站点,可能被跨站触发 prompt 测试调用
- 可能造成:
- LLM token / 配额 / 费用消耗
- prompt 调试链被滥用
- 在有权限条件下篡改 example_input / example_output
### 利用前提
- 受害者已登录
- 浏览器会带上站点 Cookie
- 目标接口接受跨站触发请求(是否可完整读取响应受 CORS 影响,但**触发请求本身**已足够构成 CSRF
### 判定
**已确认漏洞**
### 修复目标
- 为这两个接口加入 CSRF 校验
- 前端统一发送 `X-CSRF-Token` 或显式双提交 token
- 最好把“测试”和“保存示例”都收敛到 admin 路由保护体系下
---
## 四、低风险 / 加固项
## 加固项 ACSP 允许 `unsafe-inline`
### 级别
中(加固项)
### 位置
- `shared/security_headers.py`
- 线上响应头验证已确认
### 证据
源码与线上响应一致:
- `script-src 'self' 'unsafe-inline'`
- `script-src-elem 'self' 'unsafe-inline' https://cdn.jsdelivr.net`
### 风险
这不等于“当前已经有 XSS 漏洞”,但会显著削弱 CSP 对未来/潜在注入点的缓解能力。
### 建议
- 逐步迁移到 nonce/hash 驱动
- 消灭内联脚本
- 降低 XSS 成功后的利用空间
---
## 加固项 B多个 logout 允许 GET 或无 CSRF存在强制登出面
### 级别
### 位置
- `home/src/routes/pages.py:56-70`
- 公开 `/logout` 设计在 blog/canvas/prompt 也存在类似跨站触发面(主要表现为清 Cookie + 跳登录)
### 动态证据
例如:
- `GET https://blog.ephron.ren/logout?next=/admin`
- 返回 `303`,并清空 `ephron_auth`
### 风险
攻击者可诱导用户点击或加载链接,导致用户被登出。通常不构成高危,但仍属于不必要的跨站状态变更面。
### 建议
- logout 改为 POST-only
- 加入 CSRF
- 保守处理 next 参数
---
## 五、未证实但需继续关注的方向
## 1) Prompt provider / base_url 配置链的更深层 SSRF 面
本轮已确认匿名 `test-connection` 是漏洞。除此之外,还应继续确认:
- 管理后台保存 provider 配置后,正式推理链是否也能被导向任意内网地址
- 是否存在低权限用户能改该配置的路径
- 部署网络是否允许访问敏感内网资源
## 2) Blog 上传链的资源消耗风险
当前已看到:
- 扩展名限制
- 原始大小限制
- Pillow 解码后重编码为 WebP
但还应继续验证:
- 是否可被特制图片触发高内存 / 高 CPU 消耗
- 是否有 decompression bomb 风险
## 3) 登录后完整权限模型验证
本轮未进行管理员真实登录态全量遍历,因此仍需补:
- 低权限用户是否能触达部分管理面
- service token 是否在所有服务都被正确限制
- 是否存在 IDOR / 越权修改
---
## 六、已确认的安全基线(正向结论)
以下能力本轮已经确认存在:
### 1) 服务端 admin 鉴权
- blog / canvas / prompt / auth / home 都有服务端 admin 路由保护
- 未登录访问 admin 会跳转至 auth 登录页
### 2) 权限校验
- 多数写操作与管理页面都有 `require_permission(...)`
- home admin 明确拒绝 service token 访问
### 3) CSRF 防护
- 大部分后台表单写操作已实现双提交 Cookie 风格防护
- `shared/csrf.py` 逻辑完整
### 4) Cookie 基本属性合理
动态观测与源码表明认证 Cookie 具备:
- `HttpOnly`
- `SameSite=Lax`
- `Secure`
- `Domain=.ephron.ren`
### 5) 安全头存在
线上已观测到:
- `Content-Security-Policy`
- `X-Frame-Options: DENY`
- `X-Content-Type-Options: nosniff`
- `Referrer-Policy: strict-origin-when-cross-origin`
### 6) 登录与若干业务限流存在
- auth 登录限流
- blog 上传/评论/点赞限流
- home/prompt 若干 admin 写接口限流
---
## 七、优先级建议
## P0 / 立即处理
1. 关闭或修复 `POST /api/test-connection`
- 必须加鉴权
- 必须限制可请求目标
- 必须减少错误回显
## P1 / 尽快处理
2. 为 prompt 测试与保存示例接口补齐 CSRF
3. 复查 prompt 调试 API 是否应全部纳入 admin 体系
## P2 / 短期加固
4. 收紧 CSP逐步去掉 `unsafe-inline`
5. 统一 logout 为 POST + CSRF
6. 对 SSRF 风险做更系统的目标 allowlist 策略
## P3 / 持续审计
7. 登录后做完整权限/越权验证
8. 做上传/渲染/资源消耗专项测试
9. 做依赖漏洞与部署层基线检查
---
## 八、是否可以宣称“项目已安全”
**不可以。**
原因不是因为“完全没有安全措施”,而是:
- 已发现真实漏洞,且至少一个可被匿名利用
- 登录后权限与部署层仍未完成全覆盖验证
- 目前最多只能说“安全基线已具备,但尚未通过完整安全审计闭环”
更合适的对外表述应为:
> 项目已具备基础安全防护,但最新审计发现若干需要修复的问题;在完成修复与复测前,不建议宣称已完全安全。
---
## 九、后续验收标准
当以下条件全部满足后,才建议把本轮问题关闭:
1. `/api/test-connection` 需要管理员身份,且目标地址受严格限制
2. prompt 调试 / 保存示例相关接口补全 CSRF
3. 上述修复具备自动化测试覆盖
4. 复测确认匿名 SSRF 不再成立
5. 复测确认跨站无法再触发 prompt 测试/保存示例
6. CSP 至少形成去 `unsafe-inline` 的迁移计划与落地路径
---
## 十、附:本轮代表性动态证据摘要
### 匿名 SSRF 探针
- 请求:`POST https://prompt.ephron.ren/api/test-connection`
- 结果HTTP 200
- 现象:回显 `example.com` 页面片段,证明后端代发请求成立
### Prompt test 未登录受保护
- 请求:`POST https://prompt.ephron.ren/api/prompts/nonexistent/test`
- 结果HTTP 401
- 说明:该接口不是匿名开放,但仍缺 CSRF
### 各服务 admin 未登录重定向
- `blog.ephron.ren/admin` → 302 到 auth 登录
- `canvas.ephron.ren/admin` → 302 到 auth 登录
- `prompt.ephron.ren/admin` → 302 到 auth 登录
### logout 跨站可触发
- `GET https://blog.ephron.ren/logout?next=/admin` → 303 + 清 Cookie
- `GET https://canvas.ephron.ren/logout?next=/admin` → 303 + 清 Cookie
- `GET https://prompt.ephron.ren/logout?next=/admin` → 303 + 清 Cookie