add second-round security audit for ephron.ren
This commit is contained in:
318
2026-05-16-ephron-security-audit-round2.md
Normal file
318
2026-05-16-ephron-security-audit-round2.md
Normal file
@@ -0,0 +1,318 @@
|
|||||||
|
# ephron.ren 2026-05-16 全面安全审计(第二轮)
|
||||||
|
|
||||||
|
## 审计范围
|
||||||
|
|
||||||
|
本轮针对 `ephron.ren` 多服务单仓架构进行源码静态审计 + 线上非破坏性动态验证,覆盖:
|
||||||
|
|
||||||
|
- `auth`
|
||||||
|
- `blog`
|
||||||
|
- `canvas`
|
||||||
|
- `home`
|
||||||
|
- `prompt`
|
||||||
|
- `shared`
|
||||||
|
|
||||||
|
审计目标:
|
||||||
|
|
||||||
|
1. 识别匿名可利用的高风险接口
|
||||||
|
2. 核查 service token / admin / public 权限边界
|
||||||
|
3. 识别 CSRF / XSS / SSRF / 重定向 / 信息泄露类问题
|
||||||
|
4. 为修复提供可落地建议与优先级
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 结论摘要
|
||||||
|
|
||||||
|
本轮**确认 3 个需要处理的问题**,其中:
|
||||||
|
|
||||||
|
- **高危 1 个**:`prompt` 存在匿名可利用的 SSRF / 外连探测接口
|
||||||
|
- **中危 1 个**:`prompt` 调试流错误链存在潜在 HTML 注入 / XSS 风险
|
||||||
|
- **中低危 1 个**:全局 CSP 仍允许 `'unsafe-inline'`,会显著降低前端注入后的利用门槛
|
||||||
|
|
||||||
|
同时复核了一项历史上容易误报的问题:
|
||||||
|
|
||||||
|
- **开放重定向:本轮未确认成立**。`auth` 登录真正跳转前使用了 `validate_redirect()` 做同域/相对路径校验。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 漏洞 1:`prompt /api/test-connection` 可匿名触发服务端外连(确认,高危)
|
||||||
|
|
||||||
|
### 风险等级
|
||||||
|
|
||||||
|
高危
|
||||||
|
|
||||||
|
### 影响
|
||||||
|
|
||||||
|
匿名用户可直接调用 `https://prompt.ephron.ren/api/test-connection`,让服务端向任意 `base_url` 发起请求,并将网络行为结果或目标返回内容片段回显给调用者。这会带来:
|
||||||
|
|
||||||
|
1. **SSRF / 内网探测**:可探测 `127.0.0.1`、RFC1918、169.254.169.254 等地址可达性
|
||||||
|
2. **服务端网络画像泄露**:根据超时、拒绝连接、HTTP 响应差异判断服务端所处网络环境
|
||||||
|
3. **目标响应内容回显**:接口会把 `response.text[:200]` 拼到返回 JSON 中,形成受控信息泄露
|
||||||
|
4. **可被滥用为外连代理**:尽管当前固定了路径 `/chat/completions` 或 `/v1/messages`,仍可用于扫描和探测第三方或内部 HTTP 服务
|
||||||
|
|
||||||
|
### 源码证据
|
||||||
|
|
||||||
|
`prompt/src/routes/api.py`
|
||||||
|
|
||||||
|
```python
|
||||||
|
@router.post("/test-connection")
|
||||||
|
async def test_connection(payload: TestConnectionRequest):
|
||||||
|
```
|
||||||
|
|
||||||
|
该接口没有任何登录、管理员、service token 或 CSRF 前置校验。
|
||||||
|
|
||||||
|
同时其直接使用用户提供的 `base_url` 发起外连:
|
||||||
|
|
||||||
|
```python
|
||||||
|
response = await client.post(
|
||||||
|
f"{payload.base_url}/chat/completions",
|
||||||
|
```
|
||||||
|
|
||||||
|
或:
|
||||||
|
|
||||||
|
```python
|
||||||
|
response = await client.post(
|
||||||
|
f"{payload.base_url}/v1/messages",
|
||||||
|
```
|
||||||
|
|
||||||
|
并把目标响应体前 200 字符回显:
|
||||||
|
|
||||||
|
```python
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": f"HTTP {response.status_code}: {response.text[:200]}",
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 线上验证证据
|
||||||
|
|
||||||
|
匿名请求可直接成功命中该接口,且返回 200:
|
||||||
|
|
||||||
|
- `base_url=https://example.com` → 返回 `HTTP 405` 与 HTML 片段
|
||||||
|
- `base_url=http://127.0.0.1:9` → 返回 `All connection attempts failed`
|
||||||
|
- `base_url=http://169.254.169.254` → 返回 `连接超时`
|
||||||
|
- `base_url=http://10.255.255.1` → 返回 `连接超时`
|
||||||
|
|
||||||
|
这说明:
|
||||||
|
|
||||||
|
1. 接口对匿名用户开放
|
||||||
|
2. 服务端确实对调用者指定地址发起了网络连接
|
||||||
|
3. 不同目标的网络状态可以被外部观测和区分
|
||||||
|
|
||||||
|
### 复现方式
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -s https://prompt.ephron.ren/api/test-connection \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d '{
|
||||||
|
"provider":"openai",
|
||||||
|
"base_url":"http://127.0.0.1:9",
|
||||||
|
"api_key":"x",
|
||||||
|
"model":"x"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 修复建议
|
||||||
|
|
||||||
|
按优先级建议:
|
||||||
|
|
||||||
|
1. **立即加鉴权**:至少要求登录管理员;更稳妥的是仅限内部 admin UI 调用
|
||||||
|
2. **禁止任意 base_url**:仅允许测试已保存配置中的候选地址,不能由请求体任意指定
|
||||||
|
3. **加入 SSRF 防护**:拒绝私网、回环、链路本地、保留地址与裸 IP
|
||||||
|
4. **移除目标响应体回显**:只返回通用错误码/状态,不返回 `response.text`
|
||||||
|
5. **增加审计日志与限流**:记录调用者、目标 host、结果;对该接口做严格 rate limit
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 漏洞 2:`prompt` 调试流错误消息存在潜在 HTML 注入 / XSS 链(确认,中危)
|
||||||
|
|
||||||
|
### 风险等级
|
||||||
|
|
||||||
|
中危
|
||||||
|
|
||||||
|
### 影响
|
||||||
|
|
||||||
|
`prompt` 调试功能把上游 LLM/provider 返回的错误文本一路传到前端,并在浏览器中使用 `innerHTML` 渲染。如果攻击者能够控制:
|
||||||
|
|
||||||
|
- provider 的错误内容
|
||||||
|
- 或管理员配置的测试目标返回内容
|
||||||
|
- 或某些异常消息中的 HTML 片段
|
||||||
|
|
||||||
|
则存在将 HTML/脚本片段注入到调试界面的风险。
|
||||||
|
|
||||||
|
这条链当前更像是**管理/调试面 XSS 风险**,而不是匿名立即拿下;但它与漏洞 1 组合后风险会放大:匿名攻击者可借 test-connection 验证回显形态,管理员再使用调试功能时可能触发注入。
|
||||||
|
|
||||||
|
### 源码证据
|
||||||
|
|
||||||
|
后端:`prompt/src/services/llm.py`
|
||||||
|
|
||||||
|
```python
|
||||||
|
elif response.status_code != 200:
|
||||||
|
raise LLMError(f"API error: {response.text}", "api_error")
|
||||||
|
```
|
||||||
|
|
||||||
|
后端 SSE:`prompt/src/routes/api.py`
|
||||||
|
|
||||||
|
```python
|
||||||
|
except LLMError as e:
|
||||||
|
yield f"data: {json.dumps({'type': 'error', 'detail': e.message, 'code': e.code})}\n\n"
|
||||||
|
except Exception as e:
|
||||||
|
yield f"data: {json.dumps({'type': 'error', 'detail': str(e), 'code': 'unknown'})}\n\n"
|
||||||
|
```
|
||||||
|
|
||||||
|
前端:`prompt/static/js/test-prompt.js`
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
contentDiv.innerHTML = `<div class="error">${event.detail}</div>`;
|
||||||
|
```
|
||||||
|
|
||||||
|
以及:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
contentDiv.innerHTML = `<div class="error">${error.message}</div>`;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 风险链说明
|
||||||
|
|
||||||
|
完整链路为:
|
||||||
|
|
||||||
|
`provider response.text / exception text`
|
||||||
|
→ `LLMError.message`
|
||||||
|
→ SSE `event.detail`
|
||||||
|
→ 前端 `innerHTML`
|
||||||
|
|
||||||
|
如果 `event.detail` 中包含 HTML,浏览器会按 HTML 解释,而不是纯文本显示。
|
||||||
|
|
||||||
|
### 修复建议
|
||||||
|
|
||||||
|
1. 前端错误展示统一改为 `textContent`
|
||||||
|
2. 若必须保留富文本,使用严格白名单 sanitizer,而不是裸 `innerHTML`
|
||||||
|
3. 后端不要原样透传上游 `response.text`,改为固定错误文案 + 内部日志记录详细信息
|
||||||
|
4. 为该链路补充回归测试:确保 `<img onerror=...>` / `<script>` 等片段只按文本显示
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 问题 3:全局 CSP 仍允许 `'unsafe-inline'`(确认,中低危)
|
||||||
|
|
||||||
|
### 风险等级
|
||||||
|
|
||||||
|
中低危
|
||||||
|
|
||||||
|
### 影响
|
||||||
|
|
||||||
|
`shared/security_headers.py` 中的全局 CSP 仍允许:
|
||||||
|
|
||||||
|
- `script-src 'unsafe-inline'`
|
||||||
|
- `script-src-elem 'unsafe-inline'`
|
||||||
|
- `style-src 'unsafe-inline'`
|
||||||
|
|
||||||
|
这不是“单独即可利用”的漏洞,但会显著降低任何前端注入点的利用门槛,使本来可能只造成 HTML 注入的问题更容易升级为脚本执行。
|
||||||
|
|
||||||
|
### 源码证据
|
||||||
|
|
||||||
|
`shared/security_headers.py`
|
||||||
|
|
||||||
|
```python
|
||||||
|
_CSP_POLICY = (
|
||||||
|
"default-src 'self'; "
|
||||||
|
"script-src 'self' 'unsafe-inline'; "
|
||||||
|
"script-src-elem 'self' 'unsafe-inline' https://cdn.jsdelivr.net; "
|
||||||
|
"style-src 'self' 'unsafe-inline'; "
|
||||||
|
```
|
||||||
|
|
||||||
|
### 修复建议
|
||||||
|
|
||||||
|
1. 逐步移除内联脚本,迁移到静态 JS 文件
|
||||||
|
2. 使用 nonce/hash 机制替代 `'unsafe-inline'`
|
||||||
|
3. 先从高敏感页面(admin / auth / prompt test)开始收紧 CSP
|
||||||
|
4. 在 CI 中加入 CSP 违规检查,防止回退
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 已复核但未确认为漏洞的项
|
||||||
|
|
||||||
|
### 1. 开放重定向
|
||||||
|
|
||||||
|
本轮没有将其确认为漏洞。
|
||||||
|
|
||||||
|
原因:
|
||||||
|
|
||||||
|
- `auth/src/routes/pages.py` 的 `/login` 接收 `redirect/return_url/next`
|
||||||
|
- 多服务 logout 入口也可把外部 URL 编码后传给 auth
|
||||||
|
- 但真正登录成功跳转时,`auth/src/routes/api.py` 调用了:
|
||||||
|
|
||||||
|
```python
|
||||||
|
safe_redirect = validate_redirect(redirect_target)
|
||||||
|
```
|
||||||
|
|
||||||
|
而 `auth/src/utils/redirect.py` 仅允许:
|
||||||
|
|
||||||
|
- 相对路径
|
||||||
|
- `*.ephron.ren`
|
||||||
|
- 开发环境的 localhost/127.0.0.1
|
||||||
|
|
||||||
|
因此目前证据表明**真正落地跳转存在安全校验**,不应误报为已确认开放重定向。
|
||||||
|
|
||||||
|
### 2. canvas raw iframe 隔离
|
||||||
|
|
||||||
|
`canvas` 公开页使用:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
iframe.sandbox = 'allow-scripts allow-same-origin';
|
||||||
|
```
|
||||||
|
|
||||||
|
`/raw/{slug}` 又显式允许同源嵌入:
|
||||||
|
|
||||||
|
```python
|
||||||
|
"X-Frame-Options": "SAMEORIGIN"
|
||||||
|
"Content-Security-Policy": raw_csp
|
||||||
|
```
|
||||||
|
|
||||||
|
这在模型上确实增加了审计复杂度,但从当前证据看它更像是产品设计选择(同源预览能力),尚未直接证明可跨出 iframe 沙箱获得父页面控制权。因此本轮只记为**建议复核项**,暂不记为 confirmed vulnerability。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 修复优先级建议
|
||||||
|
|
||||||
|
### P0(立即处理)
|
||||||
|
|
||||||
|
1. 下线或加固 `prompt /api/test-connection`
|
||||||
|
- 加管理员鉴权
|
||||||
|
- 禁止任意 `base_url`
|
||||||
|
- 拦截私网 / 回环 / 链路本地地址
|
||||||
|
- 去掉响应体回显
|
||||||
|
|
||||||
|
### P1(本周处理)
|
||||||
|
|
||||||
|
2. 修复 `prompt` 调试流错误展示 XSS 风险
|
||||||
|
- 前端改 `textContent`
|
||||||
|
- 后端不透传原始错误体
|
||||||
|
|
||||||
|
### P2(本轮安全加固)
|
||||||
|
|
||||||
|
3. 收紧 CSP,逐步移除 `'unsafe-inline'`
|
||||||
|
|
||||||
|
### P3(继续审计)
|
||||||
|
|
||||||
|
4. 对 `canvas raw` 预览隔离模型做专项验证
|
||||||
|
5. 对全部带 `|safe` 的 Jinja 宏做来源审计,确认调用方是否只传入受信任 HTML
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 建议新增测试
|
||||||
|
|
||||||
|
1. `prompt`:匿名调用 `/api/test-connection` 应返回 401/403
|
||||||
|
2. `prompt`:拒绝私网/回环 `base_url`
|
||||||
|
3. `prompt`:错误消息渲染时 HTML 只按文本显示
|
||||||
|
4. `shared`:CSP 收紧后的回归测试
|
||||||
|
5. `canvas`:raw 页面 sandbox / CSP / frame-ancestors 组合测试
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 审计结论
|
||||||
|
|
||||||
|
当前最需要优先修的是 `prompt` 服务:
|
||||||
|
|
||||||
|
- 一条是**已可被匿名外部直接利用**的 SSRF/探测接口
|
||||||
|
- 另一条是**调试链路的潜在 XSS**
|
||||||
|
|
||||||
|
其余服务(`blog` / `canvas` / `auth` / `home`)本轮未发现同等级的新匿名高危写入或未鉴权 service API 暴露问题;service API 边界从静态与线上返回结果看总体是收紧的。
|
||||||
Reference in New Issue
Block a user