安全加固:从裸奔到还算扎实
昨天写完 thinrelay,功能跑通了就推上 GitCode。今天回头一看,安全方面基本是裸奔状态——网关用纯 HTTP 监听 0.0.0.0,API Key 明文比较,请求体无大小限制,上游错误直接透传给客户端……能用,但经不起推敲。
于是做了一轮完整的安全审查,逐文件过了一遍代码,按严重程度分了三档,然后一次性全修了。
审查过程
逐个文件读,重点关注几个方向:认证、网络传输、输入校验、错误处理、信息泄露、资源耗尽。
读完之后整理出 15 个问题,分三档:
- 严重(2 条):会被直接利用,不修会出事
- 中等(9 条):安全反模式,不会直接被打穿但留了口子
- 轻量级(4 条):锦上添花,不紧急但应该做
修了什么
严重
1. 网关没有 TLS。 代码里用的是 ListenAndServe(),纯 HTTP。但配置监听的是 8443 端口,README 里客户端连的也是 https://。实际上 API Key、OAuth token、对话内容全部明文在网络上跑,任何中间人都能抓。
修法:加了 TLSConfig 结构体,配置了 cert/key 就走 ListenAndServeTLS(),没配置就打 warn 日志提醒用反代。同时把默认监听地址从 0.0.0.0:8443 改成 127.0.0.1:8443,需要外部访问时用 Caddy/Nginx 反代。
2. 请求体无大小限制。 io.ReadAll(r.Body) 没有上限,一个 2GB 的 body 就能 OOM。
修法:加了 bodyLimitMiddleware,用 http.MaxBytesReader 限制 10MB。peekModel 里也改用 io.LimitReader。
中等
3. API Key 非常量时间比较。 != 做字符串比较会在第一个不匹配字节短路返回,理论上可以通过时序攻击逐字节猜出 key。
修法:改用 crypto/subtle.ConstantTimeCompare。
4. Codex/OpenRouter 上游无超时。 用的是 http.DefaultClient,没有 timeout。上游卡住会永远挂着连接。
修法:创建 upstreamClient,设 5 分钟超时。Codex 和 OpenRouter 都改用这个 client。
5. 错误响应泄露上游内部信息。 AGY 上游返回非 200 时,把完整响应体返回给了客户端,可能暴露 Google 内部 API 结构。
修法:对外只返回 upstream error (status),详细信息写日志。
6. Codex/OpenRouter 无限速。 限速只加在 AGY 上,Codex 和 OpenRouter 完全没有。API Key 泄露后可以无限刷配额。
修法:加了全局限速 200 RPM,叠在 auth 后面。
7. copyHeaders 透传所有上游头。 只跳了 Transfer-Encoding 和 Connection,其他全透传,可能泄露 Set-Cookie、Server 等内部头。
修法:改白名单,只透传 Content-Type、Content-Length、Cache-Control、X-RateLimit-*。
8. 默认监听 0.0.0.0。 有公网 IP 的 VPS 上任何人都能访问。
修法:默认改 127.0.0.1:8443。
9. keyring 读取有命令注入风险。 fmt.Sprintf 把 KeyringService 直接拼进 Python 代码字符串,如果值包含恶意字符可以执行任意命令。
修法:python3 改用 sys.argv 传参,不拼接字符串。
轻量级
10. 无优雅退出。 SIGTERM/SIGINT 直接杀进程,在途请求会丢。
修法:signal.Notify + server.Shutdown(),10 秒优雅退出窗口。
11. 日志无客户端 IP。 出问题没法追踪来源。
修法:logMiddleware 加 "client", r.RemoteAddr。
12. /healthz 不需认证。 暴露服务器存在性。
修法:只允许 127.0.0.1 / ::1 访问,外部请求返回 404。
13. Token 缓存 50 分钟。 Token 撤销后网关会继续用旧 token 50 分钟。
修法:缩短到 15 分钟(Codex 和 AGY 都改了)。
14. 配置明文存密钥。 config.yaml 里 API Key 是明文。
修法:支持环境变量 THINRELAY_API_KEY / THINRELAY_OPENROUTER_API_KEY 覆盖,从文件读取时打 warn 日志提醒。
15. /v1/models 硬编码。 返回的模型列表写死在代码里。
修法:改为从配置 models 字段读取,留空则用默认列表。
- 点赞
- 收藏
- 关注作者
评论(0)