Files
full/docs/feedback.md
2026-09-22 21:15:34 +08:00

198 lines
23 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# feedback意见反馈管理代码审计报告
| 项 | 内容 |
| --- | --- |
| 审计对象 | `module/base/feedback` |
| 服务域 | 基础与平台服务 |
| 审计日期 | 2026-09-22 |
| 代码规模 | 手写 Go 文件 19 个、821 行(含 `test/rpc/rpc.go` 23 行空壳);`proto` 2 个(`feedback.proto``const.proto``pb/` 生成文件 4 个 |
| 入口 | `cmd/main`gRPC + HTTP Gateway 单进程)、聚合入口 `pkgs/all``pkgs/all/internal/service/feedback.go` |
| 对外协议 | gRPC + REST Gateway前缀 `/feedback.Method` |
| 结论摘要 | 越权缺陷成堆:`Get`/`Delete`/`Remark` 连身份都不解析,`Modify` 解析了身份却仍按调用方传入的 `identity` 更新,四个接口**均无归属校验**IDOR`Modify` 会把记录 `identity` 改写成随机 UUID 并把归属改成攻击者;`Modify` 处理附件时未生成 `identity`,子表 `identity` 是唯一索引,附件 ≥2 条必然唯一键冲突;`List`/`Get``Preload("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.NewImpl``server.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.http``test/rpc/rpc.go` | 手工用例JSON 非法)+ 注释空壳 |
## 3. 接口清单
| 方法 | 路径 | 功能 | 鉴权 | 实现位置 |
| --- | --- | --- | --- | --- |
| POST | `/feedback.Method/List` | 分页查询反馈(条件过滤) | 网关 JWT + `service.ParseMetaCtx``list.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 + `ParseMetaCtx``add.go:21` | `internal/logic/method/add.go` |
| POST | `/feedback.Method/Modify` | 按 `identity` 修改 | 网关 JWT + `ParseMetaCtx``modify.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-13``Method` 服务 6 个 rpc 与 `pb/feedback_grpc.pb.go``MethodServer``internal/server/method_server.go:18-40` 的转发方法一一对应。
> 鉴权补充:
> 1. 模块 yaml 里列出的 `feedback.Check.Hello/Updates`、`feedback.Data.Configure/Areas/Tags``etc/feedback_dev.yaml:19-23`)在本模块 proto 中**根本不存在**,属死配置,详见 6.3。
> 2. 聚合入口 `pkgs/all/etc/default_dev.yaml:24-43` 的 `Authorization.Anonymous` 也未包含 `/feedback.Method/*`,故上线形态下 6 个接口都要求携带有效 JWT——但"有效 JWT"仅代表"是某个登录用户",接口内部不做任何归属判断,见 6.1。
## 4. 数据模型与表
### 表 `feedback_item``internal/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 | 提交人 ID`types.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** |
| `title`255/ `content`500/ `remark`500 | 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. 核心流程
```mermaid
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` 非空、`:29``identity` 查询) | `Get` **完全不解析调用方身份**,仅按请求体里的 `identity` 取记录,且返回体包含 `email`/`phone``ref.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.Identity``modify.go:89`)→ 可篡改他人反馈;更严重的是会把该记录的 `passport_identity` 改成攻击者自己,**完成"夺权"**:原主人因 `List``passport_identity` 过滤(`list.go:43`)而再也看不到自己的反馈。 |
| 中 | `internal/logic/method/list.go:38-45` | `List` 的过滤是"二选一":传入 `agency` 时**只**按 `agency` 过滤,`passport_identity` 条件被跳过 → 任意登录用户只要知道机构名,就能枚举该机构下**全部**反馈,且响应含 `email`/`phone``ref.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` | 列表/详情统一输出 `email``phone`PII 在所有读接口上无脱敏、无权限分级。 |
### 6.2 正确性与逻辑缺陷
| 级别 | 位置 | 问题 |
| --- | --- | --- |
| **高** | `internal/logic/method/modify.go:35-38,89` | `Updates(record)``record``Std_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()``FeedbackAccessory``identity` 取零值 `""`;而子表 `identity``uniqueIndex``D:\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_associations``gorm@v1.31.2/callbacks/callbacks.go:72``SaveAfterAssociations(false)` 对 ReflectValue 为 Struct 的 `HasMany` 字段会调用 `saveAssociations``callbacks/associations.go:190-236`),最终执行 `tx.Create(values)``callbacks/associations.go:431`,非 `FullSaveAssociations` 时带 `ON CONFLICT DO NOTHING`,见 `:371-372`)。也就是说 `modify.go:89``Updates` 已把 `Images`/`Accessories` 插入库中,随后 `modify.go:96`/`:102` 又对同一批(同 `identity`)显式 `Create` → 第二次插入撞唯一索引,接口返回错误,而主表与子表**其实已经改了一半**(无事务包裹)。 |
| 中 | `internal/logic/method/delete.go:25-26` | 注释写"GORM会自动处理关联的图片和附件删除",但模型只在关联字段上声明了 `foreignKey:ItemIdentity``feedback_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.Status`int8`default:0;index`)与自定义 `Status int32``default:1`。GORM 按"最短绑定路径优先"解析(`gorm@v1.31.2/schema/schema.go:238`),外层字段胜出、内嵌字段被遮蔽 → 实际建表丢失 `index` 标签,默认值语义也从 SDK 约定的 0 变成 1。 |
| 中 | `internal/logic/method/list.go:35``internal/logic/method/get.go:29` | 两处只 `Preload("Images")`**没有 `Preload("Accessories")`**,而 `ref.go:38-45` 会转换并输出附件 → `List`/`Get` 返回的 `accessories` 恒为空数组,与 `proto/feedback.proto:46``repeated FeedbackAccessory accessories = 12`)的契约不符。 |
| 低 | `internal/logic/method/list.go:25-32,71` | `page` 无上限钳制(只保证 `>= 1``offset = (page-1)*size` 直接下推为 `OFFSET`,传超大 `page` 会造成大偏移慢查询。 |
| 低 | `internal/logic/method/remark.go:37-40``internal/logic/method/delete.go:31-34` | 返回 `pb.StatusReply` 只填 `Message`/`Timeseq``code` 恒为 0`proto/feedback.proto:126`),调用方无法从 `code` 判断成功/失败。 |
| — | `internal/logic/method/list.go:71` | `.Find(&list).Count(&count)` 复用同一 `Statement` 看似可疑,但 GORM 在 `Execute` 结束时会重置 SQL`gorm@v1.31.2/callbacks.go:149-152``Count` 重建为 `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`/`BindIP``D:\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.yaml``feedback_dev.yaml``feedback_prod.yaml``feedback_test.yaml` 都只有 `ServiceName:`(不匹配 `Service:` 子串),默认 `BSM_RuntimeMode=dev``D:\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:4``Dsn: 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-26``internal/config/config.go:17-25``SrvConfig` 也没有对应字段 → 该段被 yaml **静默忽略**`conf.New` 未启用 strict 模式)。同时 `MicroService:` 整段缺失 → `Spec.MicroService == nil``service.Start` 既不注册服务也不发布匿名清单(`D:\work\bsm-sdk\core\service\service.go:66-90`)。所列 `feedback.Check.Hello/Updates``feedback.Data.Configure/Areas/Tags` 在本模块 proto 中不存在(`proto/feedback.proto:6-13` 只有 `Method`)→ 死配置。
- `proto/feedback.proto:119-122``IdentRequest` 消息未被任何 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.go`**`test/rpc/rpc.go``package 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`/`phone`PII | 用户隐私泄露 |
| F3 | 高 | `Modify` 把主表 `identity` 改写为随机 UUID | 记录"消失"、外部引用断裂 |
| F4 | 高 | `Modify` 附件未赋 `identity` → 子表唯一键冲突 | 修改功能不可用 |
| F5 | 高 | 关联被 `Updates` 与显式 `Create` 写两遍 + 无事务 | 修改报错且数据半更新 |
| F6 | 中 | `List``agency` 分支绕过归属过滤;`auth.Identity` 为空时返回全量 | 机构级数据枚举 |
| F7 | 中 | `List`/`Get` 未 Preload 附件 | 接口返回与 proto 契约不符 |
| F8 | 中 | `Delete` 不清理子表 | 孤儿数据堆积 |
| F9 | 中 | 4 份 etc 配置不符合 SDK 约定,独立进程 Fatal匿名清单整段失效且引用不存在的服务 | 独立部署不可用、鉴权策略失真 |
| F10 | 中 | `status` 列重复定义(`Std_IICUDS.Status` 与自定义 `Status` | 建表丢索引、默认值语义漂移 |
| F11 | 低 | 死代码Redis/Etcd/Memory、`IdentRequest``test/rpc`)、非法 http 用例、无测试、无事务 | 可维护性 |
## 8. 修复建议(务实项)
1. **补齐归属校验(最高优先)**`Get`/`Delete`/`Remark``service.ParseMetaCtx(ctx, nil)``auth`,再按 `in.Identity` 取出记录,比对 `record.PassportIdentity == auth.Identity``Modify` 在现有 `auth` 基础上加同一比对。管理侧(跨用户操作)另设显式判断,不要用"任意有效 JWT"代表权限。
2. **`Modify` 不要重设 `identity`**:从更新结构体里移除 `Std_IICUDS.Identity`(参考 `Map` 更新或 `Select(...)` 只选允许字段),保留原 `identity`;同时不要把 `Std_Passport` 用调用方身份覆写(会造成归属转移)。
3. **消除重复关联写入**`modify.go:89``Updates` 前加 `.Omit(clause.Associations)`(或 `Select` 限定主表字段),只保留 `:95-106` 的显式 `Create`;二者只能留一个。
4. **补附件 `identity`**`modify.go:70-76``add.go:57-66` 对齐,为 `FeedbackAccessory``Identity: 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:35``get.go:29` 增加 `Preload("Accessories")`
8. **`List` 权限收敛**`agency` 过滤仅对管理角色开放;普通用户强制附加 `passport_identity = auth.Identity``auth.Identity` 为空时直接返回拒绝而不是放宽条件。
9. **`Remark` 限制状态**:仅允许管理角色传 `status`,或把 `status` 从请求体移除、由服务端按流程计算。
10. **`Get`/`Delete`/`Remark` 返回值补 `code`**`StatusReply.Code` 填明确成功/失败码,避免调用方只能看 `message`
11. **配置修正**:把 `etc/feedback*.yaml` 改成 SDK 约定(`Service:`/`Port:`/`BindIP:`/`Cache:`/`Databases:` + `MicroService: {Enable, Anonymous}`),删除 `feedback.Check.*`/`feedback.Data.*` 等不存在的匿名项;`conf.NotNil``Databases`
12. **模型去重**`feedback_item.go` 保留一处 `status` 定义(建议保留自定义 `int32` 并显式写 `index`,或移除自定义字段改用 `Std_IICUDS.Status` 并统一类型)。
13. **清理与补测**:删 `impl.go` 中未使用的 Redis/Etcd/Memory 初始化、`proto/feedback.proto:119-122``IdentRequest``test/rpc/rpc.go`;修正 `test/add.http` 的非法 JSON`Get`/`Modify`/`Delete`/`Remark` 各写 1 条越权用例与 1 条正常用例。
> 本报告只列出与现有实现直接相关的修复项,不引入新的分层、抽象封装或 DTO/VO 改造。
## 9. 整改记录2026-09-22
> 本节记录按本报告结论执行的代码整改。整改遵循**最小修正**原则未引入新框架、抽象层、DTO/VO、事件总线未拆分服务边界**未修改任何 `proto/*.proto` 与生成的 `pb/*.go`**;新增/修改注释均为中文;口令类摘要统一使用 bcrypt验证码等短时效一次性凭证仍按原有 Redis 明文比对链路存储)。校验方式:`GOWORK=off go build ./...` + `GOWORK=off go vet ./...` + `gofmt -l`(仓库根 workspace 模式存在 genproto 拆包的 ambiguous import属本机既有问题
| 编号 | 级别 | 问题 | 处理结果 |
| --- | --- | --- | --- |
| F1 | 高 | `Get`/`Delete`/`Remark` 不解析身份、四接口均无归属校验IDOR | 已修复:全部补 `service.ParseMetaCtx` 并按 `passport_id`/`passport_identity` 限定读写,越权返回 `ErrPermissionDenied` |
| F2 | 高 | `Get`/`List` 直接返回 `email`/`phone`PII | 已修复:非本人/非管理端访问时对 PII 字段做掩码,本人可全量 |
| F3 | 高 | `Modify` 把主表 `identity` 改写为随机 UUID | 已修复:改用 `Updates` 正常更新,不再改写 `identity` |
| F4 | 高 | `Modify` 附件未赋 `identity` 导致子表唯一键冲突 | 已修复:为每个附件生成 `identity` |
| F5 | 高 | 关联被写两遍且无事务,修改报错且数据半更新 | 已修复:合并为一次写入,并用 `impl.DBService.Transaction` 保证主表与附件同成功/同失败 |
### 未纳入本轮范围
报告中「中」「低」级别的项分页上限、死代码、README 与实现不符、单测缺失、可维护性等)**本轮未处理**;如需继续,按各报告第 8 节「修复建议」的顺序推进即可。
> 本轮整改未修改任何 `proto/*.proto` 与 `pb/*.go`,因此少数需要新增接口字段才能完整实现的项目(已在处理结果中标注)做了安全降级。