Files
full/docs/feedback.md
2026-09-22 18:53:53 +08:00

21 KiB
Raw Blame History

feedback意见反馈管理代码审计报告

内容
审计对象 module/base/feedback
服务域 基础与平台服务
审计日期 2026-09-22
代码规模 手写 Go 文件 19 个、821 行(含 test/rpc/rpc.go 23 行空壳);proto 2 个(feedback.protoconst.protopb/ 生成文件 4 个
入口 cmd/maingRPC + HTTP Gateway 单进程)、聚合入口 pkgs/allpkgs/all/internal/service/feedback.go
对外协议 gRPC + REST Gateway前缀 /feedback.Method
结论摘要 越权缺陷成堆:Get/Delete/Remark 连身份都不解析,Modify 解析了身份却仍按调用方传入的 identity 更新,四个接口均无归属校验IDORModify 会把记录 identity 改写成随机 UUID 并把归属改成攻击者;Modify 处理附件时未生成 identity,子表 identity 是唯一索引,附件 ≥2 条必然唯一键冲突;List/GetPreload("Images"),附件永远为空;此外 etc/ 下 4 份 yaml 全部不符合 SDK 配置约定(Name/ListenOn/Dsn + 顶层 Anonymous),独立进程启动即 log.Fatalln

1. 服务定位与职责

用户意见/反馈的提交与后台管理:提交反馈(含图片、附件)、按条件分页查询、查看详情、修改、删除、填写备注并流转状态。数据模型为"反馈主表 + 图片子表 + 附件子表"。它不负责附件文件本身的上传(那是 module/base/fts 的职责),只保存 URL/路径。

2. 代码结构与入口

路径 职责
cmd/main/main.go 独立进程入口:config.New("Feedback")impl.NewImplserver.New(nil)service.New(...).Start()
cmd/cli/main.go 命令行入口,仅 log.Println("Hello World!")7 行,占位)
internal/config/config.go 配置结构(Base/Databases/MicroService/Rpc/Gateway/APM/Etcd)与 conf.NotNil 校验
internal/impl/impl.go 初始化 DB / Redis / Etcd / 内存缓存
internal/logic/method/list.go List:分页 + 按 identity/agency/username/status/category 过滤
internal/logic/method/get.go Get:按 identity 查详情
internal/logic/method/add.go Add:创建主表 + 图片 + 附件
internal/logic/method/modify.go Modify:删子表 → 更新主表 → 重建子表
internal/logic/method/delete.go Delete:按 identity 删主表
internal/logic/method/remark.go Remark:更新 remark + status
internal/logic/method/ref.go convert:模型 → pb 转换(含图片、附件)
internal/models/feedback_item.go feedback_item 主表模型 + 迁移注册
internal/models/feedback_images.go feedback_images 图片子表
internal/models/feedback_accessory.go feedback_accessory 附件子表
internal/server/{new,method_server}.go 注册 Method 到 gRPChandler 转发 logic
internal/routers 不存在(走 gRPC + HTTP Gateway
service/{expose,dependencies}.go 聚合宿主注入接口
etc/{feedback.yaml,feedback_dev,feedback_prod,feedback_test}.yaml 4 份配置,字段命名与 SDK 约定不符(见 6.3
test/add.httptest/rpc/rpc.go 手工用例JSON 非法)+ 注释空壳

3. 接口清单

方法 路径 功能 鉴权 实现位置
POST /feedback.Method/List 分页查询反馈(条件过滤) 网关 JWT + service.ParseMetaCtxlist.go:19 internal/logic/method/list.go
POST /feedback.Method/Get identity 查详情(返回 email/phone 网关 JWT进程内不解析身份get.go 无 ParseMetaCtx internal/logic/method/get.go
POST /feedback.Method/Add 新增反馈 网关 JWT + ParseMetaCtxadd.go:21 internal/logic/method/add.go
POST /feedback.Method/Modify identity 修改 网关 JWT + ParseMetaCtxmodify.go:24 internal/logic/method/modify.go
POST /feedback.Method/Delete identity 删除 网关 JWT进程内不解析身份delete.go internal/logic/method/delete.go
POST /feedback.Method/Remark identity 更新备注 + 状态 网关 JWT进程内不解析身份remark.go internal/logic/method/remark.go

声明但未实现的接口:无。proto/feedback.proto:6-13Method 服务 6 个 rpc 与 pb/feedback_grpc.pb.goMethodServerinternal/server/method_server.go:18-40 的转发方法一一对应。

鉴权补充:

  1. 模块 yaml 里列出的 feedback.Check.Hello/Updatesfeedback.Data.Configure/Areas/Tagsetc/feedback_dev.yaml:19-23)在本模块 proto 中根本不存在,属死配置,详见 6.3。
  2. 聚合入口 pkgs/all/etc/default_dev.yaml:24-43Authorization.Anonymous 也未包含 /feedback.Method/*,故上线形态下 6 个接口都要求携带有效 JWT——但"有效 JWT"仅代表"是某个登录用户",接口内部不做任何归属判断,见 6.1。

4. 数据模型与表

feedback_iteminternal/models/feedback_item.go

字段 类型 键/约束 说明
id uint PK 来自 types.Std_IICUDS
identity varchar(36) uniqueIndex 记录唯一标识(D:\work\bsm-sdk\core\types\db.go:30
created_at/updated_at/deleted_at timestamp - 带软删
passport_id uint Index 提交人 IDtypes.Std_Passport
passport_identity varchar(36) Index 提交人标识,List 的归属过滤字段
status int32 default 1 1 未处理 / 2 已处理(与内嵌 Std_IICUDS.Status int8 同列冲突,见 6.2
category / user_name / email / phone varchar - 分类 / 用户名 / 邮箱 / 手机(含 PII
title255/ content500/ remark500 varchar - 标题 / 内容 / 备注
agency varchar(255) - 机构
images / accessories - foreignKey:ItemIdentity GORM 关联字段(非列)

feedback_images / feedback_accessory

字段 类型 键/约束 说明
identity varchar(36) uniqueIndex 子表唯一标识(来自 Std_IICUDS
item_identity varchar(36) default '' 关联主表 identity,无 DB 级外键
url(图片) varchar(255) default '' 图片地址
title / file_path(附件) varchar(255) / varchar(500) not null 附件标题 / 附件路径

5. 核心流程

flowchart TD
    A["POST /feedback.Method/Modify"] --> B["service.ParseMetaCtx 解析 auth"]
    B --> C["校验 in.Identity 非空"]
    C --> D["构造 recordIdentity = utils.UUID(),归属改写为 auth"]
    D --> E["按 item_identity 删除 feedback_images / feedback_accessory"]
    E --> F["Where(identity = in.Identity).Updates(record)"]
    F --> G["GORM 顺带 upsert 非空 HasMany 关联"]
    G --> H["显式 Create(&record.Images) / Create(&record.Accessories)"]
    I["POST /feedback.Method/Get"] --> J["Where(identity = in.Identity).First无身份、无归属校验"]
    J --> K["返回含 email / phone 的完整记录"]

6. 审计发现

6.1 安全

级别 位置 问题
internal/logic/method/get.go:19-41(关键::21 只校验 identity 非空、:29identity 查询) Get 完全不解析调用方身份,仅按请求体里的 identity 取记录,且返回体包含 email/phoneref.go:17-18)。任何持有效 JWT 的用户遍历 identity 即可读取全部用户的反馈与联系方式IDOR + PII 泄露)。
internal/logic/method/delete.go:19-29(关键::26 Delete 不解析身份、不做归属校验Where("identity = ?", in.GetIdentity()).Delete(...) 直接删除。任何登录用户可删除他人反馈(软删,deleted_at)。
internal/logic/method/remark.go:19-35(关键::21,:28,:32 Remark 不解析身份、不做归属校验,且 status 取自请求体(remark.go:28)→ 任何登录用户可改写任意反馈的备注与处理状态(可把"已处理"回退或伪造)。
internal/logic/method/modify.go:22-27,30,89 Modify 虽然解析了 auth,但只用它覆写归属字段modify.go:39-42),更新条件仍是调用方传入的 in.Identitymodify.go:89)→ 可篡改他人反馈;更严重的是会把该记录的 passport_identity 改成攻击者自己,完成"夺权":原主人因 Listpassport_identity 过滤(list.go:43)而再也看不到自己的反馈。
internal/logic/method/list.go:38-45 List 的过滤是"二选一":传入 agencyagency 过滤,passport_identity 条件被跳过 → 任意登录用户只要知道机构名,就能枚举该机构下全部反馈,且响应含 email/phoneref.go:17-18)。
internal/logic/method/list.go:41-44 auth.Identity 为空串时,passport_identity 过滤被跳过(if userIdentity != ""),查询退化为无归属条件的分页查询。service.ParseMetaCtx 只校验 JWT 签名与有效期、不校验 identity 非空(D:\work\bsm-sdk\core\service\meta.go:19-47),因此该分支在 token 未携带 identity 时可达。
internal/logic/method/ref.go:13-26 列表/详情统一输出 emailphonePII 在所有读接口上无脱敏、无权限分级。

6.2 正确性与逻辑缺陷

级别 位置 问题
internal/logic/method/modify.go:35-38,89 Updates(record)recordStd_IICUDS.Identity = utils.UUID()(新 UUID。GORM 结构体 Updates 只忽略零值,Identity 非零 → 主表 identity 列被改写成新 UUID。由于 Where 用的是旧 identity,更新后原 identity 立即失效,Get/Modify/Delete/Remark 全部找不到该记录,任何外部引用(如工单、通知)随之断裂。
internal/logic/method/modify.go:70-76 构造附件时未设置 Identity(对比 add.go:57-66 明确赋 utils.UUID()FeedbackAccessoryidentity 取零值 "";而子表 identityuniqueIndexD:\work\bsm-sdk\core\types\db.go:30)→ 附件 ≥2 条时必然唯一键冲突;即使只传 1 条,也会写入一行 identity="" 的脏数据,后续再改就再也插不进去。
internal/logic/method/modify.go:50-52,89,95-106 关联被写入两遍。GORM v2 的 Update 处理器注册了 gorm:save_after_associationsgorm@v1.31.2/callbacks/callbacks.go:72SaveAfterAssociations(false) 对 ReflectValue 为 Struct 的 HasMany 字段会调用 saveAssociationscallbacks/associations.go:190-236),最终执行 tx.Create(values)callbacks/associations.go:431,非 FullSaveAssociations 时带 ON CONFLICT DO NOTHING,见 :371-372)。也就是说 modify.go:89Updates 已把 Images/Accessories 插入库中,随后 modify.go:96/:102 又对同一批(同 identity)显式 Create → 第二次插入撞唯一索引,接口返回错误,而主表与子表其实已经改了一半(无事务包裹)。
internal/logic/method/delete.go:25-26 注释写"GORM会自动处理关联的图片和附件删除",但模型只在关联字段上声明了 foreignKey:ItemIdentityfeedback_item.go:23-24没有 OnDelete:CASCADE,子表也无 DB 级外键 → Delete 只删主表,feedback_images/feedback_accessory 成为孤儿数据(Modify 是手动先删子表的,Delete 没做)。
internal/models/feedback_item.go:12:18 status 列被定义两次:内嵌 types.Std_IICUDS.Statusint8default:0;index)与自定义 Status int32default:1。GORM 按"最短绑定路径优先"解析(gorm@v1.31.2/schema/schema.go:238),外层字段胜出、内嵌字段被遮蔽 → 实际建表丢失 index 标签,默认值语义也从 SDK 约定的 0 变成 1。
internal/logic/method/list.go:35internal/logic/method/get.go:29 两处只 Preload("Images")没有 Preload("Accessories"),而 ref.go:38-45 会转换并输出附件 → List/Get 返回的 accessories 恒为空数组,与 proto/feedback.proto:46repeated FeedbackAccessory accessories = 12)的契约不符。
internal/logic/method/list.go:25-32,71 page 无上限钳制(只保证 >= 1offset = (page-1)*size 直接下推为 OFFSET,传超大 page 会造成大偏移慢查询。
internal/logic/method/remark.go:37-40internal/logic/method/delete.go:31-34 返回 pb.StatusReply 只填 Message/Timeseqcode 恒为 0proto/feedback.proto:126),调用方无法从 code 判断成功/失败。
internal/logic/method/list.go:71 .Find(&list).Count(&count) 复用同一 Statement 看似可疑,但 GORM 在 Execute 结束时会重置 SQLgorm@v1.31.2/callbacks.go:149-152Count 重建为 SELECT count(*) ... LIMIT n OFFSET m,聚合结果单行不受 LIMIT 影响,count 等于命中总数。非缺陷(特此说明)。
internal/logic/method/add.go:36 Add 接受调用方传入的 status,与 proto/feedback.proto:74 注释"也可以调用方自行设置"一致,属设计约定;但结合 6.1 的 Remark 问题,状态流转整体缺少权限约束。

6.3 未完成实现

  • 独立进程无法启动4 份 etc 配置全不符合 SDK 约定etc/feedback_dev.yaml:1-4 使用 Name: / ListenOn: / Dsn:,而 SDK 的 conf.Base yaml 标签是 Service/Port/Cache/SecretKey/BindIPD:\work\bsm-sdk\core\conf\types.go:7-16);更关键的是 conf.New 在解析前会校验文件文本必须包含 Service:,否则 log.Fatalln("ERROR: Service Not Nil")D:\work\bsm-sdk\core\conf\new.go:58-60)。feedback.yamlfeedback_dev.yamlfeedback_prod.yamlfeedback_test.yaml 都只有 ServiceName:(不匹配 Service: 子串),默认 BSM_RuntimeMode=devD:\work\bsm-sdk\core\env\env.go:20)会加载 etc/feedback_dev.yaml进程直接 Fatal 退出。即便绕过该校验,因无 Databases: 段,with.Databases 也会 panic("No Database Source Found !")D:\work\bsm-sdk\core\with\databases.go:13-15)。etc/feedback_prod.yaml:4Dsn: postgres://prod:***@192.168.0.224:5432/scf...,即"生产配置"同样不可用。
  • 匿名清单整段失效且内容不存在etc/feedback_dev.yaml:16-23 用的是顶层 Anonymous: + Key:/Urls: 结构,而 SDK 期望 MicroService: {Enable: bool, Anonymous: []string}conf\types.go:23-26internal/config/config.go:17-25SrvConfig 也没有对应字段 → 该段被 yaml 静默忽略conf.New 未启用 strict 模式)。同时 MicroService: 整段缺失 → Spec.MicroService == nilservice.Start 既不注册服务也不发布匿名清单(D:\work\bsm-sdk\core\service\service.go:66-90)。所列 feedback.Check.Hello/Updatesfeedback.Data.Configure/Areas/Tags 在本模块 proto 中不存在(proto/feedback.proto:6-13 只有 Method)→ 死配置。
  • proto/feedback.proto:119-122IdentRequest 消息未被任何 rpc 引用 → 死消息。
  • 未发现显式 TODO/占位。

6.4 健壮性与可维护性

级别 位置 问题
internal/config/config.go:39 conf.NotNil(Spec.Service, Spec.Cache) 未校验 Databases(与 ads 等模块同模式),配合 with.Databases 的 panic缺配置时表现为启动崩溃且报错不含配置项名。
internal/logic/method/modify.go:79-106 删子表 → 更新主表 → 建子表 三步没有事务包裹(对比 add.go:69 的单次 Create 由 GORM 自动开事务),任一步失败都会留下半成品数据。
internal/impl/impl.go:13-16 RedisService/EtcdService/MemorySerice 初始化后本模块无任何使用点(死代码,同 ads
test/add.http:9,16 用例 JSON 含 "type" 字段proto 无此字段)且末尾多一个逗号(非法 JSON无法直接执行status 未传。
test/rpc/rpc.go:3-23 整个 main 被注释,且引用不存在的 pb.NewMethodClient/pb.Crc → 死代码。
全模块 无任何 *_test.gotest/rpc/rpc.gopackage rpc 下且函数体全注释),上述 IDOR 与附件唯一键冲突均无测试覆盖。
internal/logic/method/modify.go:56-59 图片 identity 缺省时生成新 UUID意味着"不传 identity 就重建",与"按 identity 幂等更新"的语义不一致;结合 6.2 的重复写入问题,同一图片会被覆盖或冲突。

7. 风险汇总

编号 级别 问题 影响面
F1 Get/Delete/Remark 不解析身份 + 四接口均无归属校验IDOR 越权读取/删除/篡改他人反馈
F2 Get/List 返回 email/phonePII 用户隐私泄露
F3 Modify 把主表 identity 改写为随机 UUID 记录"消失"、外部引用断裂
F4 Modify 附件未赋 identity → 子表唯一键冲突 修改功能不可用
F5 关联被 Updates 与显式 Create 写两遍 + 无事务 修改报错且数据半更新
F6 Listagency 分支绕过归属过滤;auth.Identity 为空时返回全量 机构级数据枚举
F7 List/Get 未 Preload 附件 接口返回与 proto 契约不符
F8 Delete 不清理子表 孤儿数据堆积
F9 4 份 etc 配置不符合 SDK 约定,独立进程 Fatal匿名清单整段失效且引用不存在的服务 独立部署不可用、鉴权策略失真
F10 status 列重复定义(Std_IICUDS.Status 与自定义 Status 建表丢索引、默认值语义漂移
F11 死代码Redis/Etcd/Memory、IdentRequesttest/rpc)、非法 http 用例、无测试、无事务 可维护性

8. 修复建议(务实项)

  1. 补齐归属校验(最高优先)Get/Delete/Remarkservice.ParseMetaCtx(ctx, nil)auth,再按 in.Identity 取出记录,比对 record.PassportIdentity == auth.IdentityModify 在现有 auth 基础上加同一比对。管理侧(跨用户操作)另设显式判断,不要用"任意有效 JWT"代表权限。
  2. Modify 不要重设 identity:从更新结构体里移除 Std_IICUDS.Identity(参考 Map 更新或 Select(...) 只选允许字段),保留原 identity;同时不要把 Std_Passport 用调用方身份覆写(会造成归属转移)。
  3. 消除重复关联写入modify.go:89Updates 前加 .Omit(clause.Associations)(或 Select 限定主表字段),只保留 :95-106 的显式 Create;二者只能留一个。
  4. 补附件 identitymodify.go:70-76add.go:57-66 对齐,为 FeedbackAccessoryIdentity: utils.UUID()
  5. Delete 清理子表:按 item_identity = in.Identity 先删 feedback_images/feedback_accessory,再删主表(与 Modify 的现有动作一致)。
  6. 事务:把 modify.go:79-106 的删/更新/建三步放进 impl.DBService.Transaction(func(tx *gorm.DB) error {...})
  7. Preload 补齐list.go:35get.go:29 增加 Preload("Accessories")
  8. List 权限收敛agency 过滤仅对管理角色开放;普通用户强制附加 passport_identity = auth.Identityauth.Identity 为空时直接返回拒绝而不是放宽条件。
  9. Remark 限制状态:仅允许管理角色传 status,或把 status 从请求体移除、由服务端按流程计算。
  10. Get/Delete/Remark 返回值补 codeStatusReply.Code 填明确成功/失败码,避免调用方只能看 message
  11. 配置修正:把 etc/feedback*.yaml 改成 SDK 约定(Service:/Port:/BindIP:/Cache:/Databases: + MicroService: {Enable, Anonymous}),删除 feedback.Check.*/feedback.Data.* 等不存在的匿名项;conf.NotNilDatabases
  12. 模型去重feedback_item.go 保留一处 status 定义(建议保留自定义 int32 并显式写 index,或移除自定义字段改用 Std_IICUDS.Status 并统一类型)。
  13. 清理与补测:删 impl.go 中未使用的 Redis/Etcd/Memory 初始化、proto/feedback.proto:119-122IdentRequesttest/rpc/rpc.go;修正 test/add.http 的非法 JSONGet/Modify/Delete/Remark 各写 1 条越权用例与 1 条正常用例。

本报告只列出与现有实现直接相关的修复项,不引入新的分层、抽象封装或 DTO/VO 改造。