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

25 KiB
Raw Permalink Blame History

cms站点内容管理代码审计报告

内容
审计对象 module/base/cms
服务域 基础与平台服务(内容管理:文章 / 页面 / 分类 / 标签 / 评论 / 赞踩)
审计日期 2026-09-22
代码规模 手写 Go 文件约 62 个logic 41、models 10、server 6、service 2、config 1、impl 1、utils 1、cmd 1pb 生成代码 17 个post/pages/tags/site/category 三件套 + const + blocks_compatproto 6 个;.http 测试 36 个;无 cmd/main、无 yaml 配置、无 *_test.go 单测
入口 仅聚合入口 pkgs/allpkgs/ecmall(经 service.Expose 注册 gRPC + HTTP Gatewaycmd/cli/main.go 仅为 Hello World 占位,无独立可运行进程
对外协议 gRPCcms.Post/cms.Pages/cms.Category/cms.Tags/cms.Site+ grpc-gateway HTTP/cms.<Svc>/<Method>);站点 Site 系列为声明但未实现的占位
结论摘要 文章/页面查询、评论、计数增改等核心逻辑已实现,但存在评论发布 panic未命中 mall_staff 即崩溃)赞/踩无去重且可刷成负数删除父评论时文章评论计数不一致Search 无分页上限(可拖全表)文章/评论增删改不校验归属(越权)Site 全系列占位空实现等真实缺陷。

1. 服务定位与职责

站点内容管理CMS微服务提供文章、单页、分类、标签的 CRUD 与查询,以及评论、点赞/踩等互动能力。数据模型以 cms_post/cms_pages/cms_category/cms_tags/cms_comment 为核心,通过 cms_relate_category/cms_relate_tags/cms_accessory 维护关联。它是内容存储与查询服务,不负责全文检索、富媒体转码或内容审核(这些不在本模块代码内)。Site(站点配置)在 proto 与 server 中均已声明并注册,但逻辑层全部为占位空实现。

2. 代码结构与入口

路径 职责
cmd/cli/main.go 占位入口,仅打印 Hello World,不启动任何服务
service/expose.go 聚合宿主注入依赖 + 注册 5 个 gRPC 服务与 5 个 Gateway Handler
service/dependencies.go DependenciesRedis/Etcd/DB/Cache覆盖 internal/impl 全局对象
internal/config/config.go 配置结构(Base/Databases/MicroService/Rpc/Gateway/Apm/Etcd)与校验
internal/impl/impl.go 初始化 Redis、DB、Etcd、内存缓存为全局对象
internal/server/new.go gRPC Server 注册入口(Register*Server
internal/server/{post,pages,category,tags,site}_server.go gRPC 服务实现,转发到对应 logic 包
internal/logic/post/* 文章 CRUD、查询、搜索、评论、赞/踩20 文件)
internal/logic/category/* internal/logic/tags/* internal/logic/pages/* internal/logic/site/* 分类、标签、页面、站点逻辑
internal/models/*.go 9 张表模型 + query.go(增删改查与计数)
internal/utils/string.go FormatKey(小写、去空格)
proto/*.proto pb/*.go 接口契约与生成代码(仅看签名)
test/**/*.http HTTP 调用示例(非自动化测试)

依赖注入:service.Dependencies 由聚合入口 pkgs/all 传入共享连接Redis/Etcd/DB/Memory非 nil 时覆盖 internal/impl 全局对象;config.NotNil 仅校验 Service/Cache

3. 接口清单

鉴权列:除 CommentList 外,所有 handler 均调用 service.ParseMetaCtx(ctx, nil) 解析登录态(无 token 即报错),即已认证、未授权(不校验资源归属)。Site 系列虽调用 ParseMetaCtx,但后续无实际操作。

服务 方法 proto / 路径 功能 鉴权 实现位置 状态
Post Fetch /cms.Post/Fetch 文章分页列表(按分类/关键词/类型) logic/post/fetch.go 已实现
Post GetByIdentity /cms.Post/GetByIdentity 按 identity 获取详情(含点击量+1 logic/post/get_by_identity.go 已实现
Post GetByKey /cms.Post/GetByKey 按 key(hash) 获取详情 logic/post/get_by_key.go 已实现
Post Search /cms.Post/Search 标题/正文关键词搜索 logic/post/search.go 已实现(分页缺陷)
Post Create /cms.Post/Create 创建文章+关联 logic/post/create.go 已实现
Post Modify /cms.Post/Modify 修改文章 logic/post/modify.go 已实现(无归属校验)
Post Delete /cms.Post/Delete 删除文章+关联 logic/post/delete.go 已实现(无归属校验)
Post IncrPostLike / DescPostLike /cms.Post/IncrPostLike 文章点赞/取消 logic/post/incr_post_like.go 等 4 文件 已实现(无去重)
Post CommentList /cms.Post/CommentList 评论分页(树形) 无(已注释) logic/post/comment_list.go:24-27 已实现
Post AddComment /cms.Post/AddComment 发布评论(更新计数) logic/post/add_comment.go 已实现panic 风险)
Post ModifyComment /cms.Post/ModifyComment 修改评论内容 logic/post/modify_comment.go 已实现(无归属校验)
Post DeleteComment /cms.Post/DeleteComment 删除评论(含子级) logic/post/delete_comment.go 已实现(无归属校验)
Post IncrCommentLike/DescCommentLike/IncrCommentUnlike/DescCommentUnlike /cms.Post/... 评论赞/踩 logic/post/incr_comment_like.go 等 4 文件 已实现(无去重)
Category Fetch/Create/Modify/Delete /cms.Category/... 分类 CRUD + 树形列表 logic/category/* 已实现
Pages Fetch/GetByIdentity/GetByKey/Create/Modify/Delete /cms.Pages/... 页面 CRUD + 查询 logic/pages/* 已实现GetByIdentity 点击量不增)
Tags Fetch/Create/Modify/Delete /cms.Tags/... 标签 CRUD + 列表 logic/tags/* 已实现
Site Fetch /cms.Site/Fetch 站点列表 logic/site/fetch.go 占位:返回空 site/fetch.go:18,20
Site Get /cms.Site/Get 站点详情 logic/site/get.go 占位:返回 nil site/get.go:24
Site Create /cms.Site/Create 新增站点 logic/site/create.go 占位:成功但不落库 site/create.go:20-22
Site Modify /cms.Site/Modify 修改站点 logic/site/modify.go 占位:成功但不落库 site/modify.go:21-23
Site Delete /cms.Site/Delete 删除站点 logic/site/delete.go 占位:成功但不删 site/delete.go:27

对照 protopost.proto/pages.proto/category.proto/tags.proto/site.proto)与 internal/server/*_server.go + service/expose.go5 个服务的所有 rpc 均已在 gRPC 与 Gateway 注册;不存在"声明但未注册"的方法。但 Site 全部 5 个方法为"已注册、无逻辑"的占位实现(代码内含 // TODO: add your logic code & delete this line.),且 cms_site 表已通过 init() 纳入自动迁移,却无任何写入路径——属于"声明但未实现"。

4. 数据模型与表

模型 gorm tag 存在反直觉之处:cms_post.typesint32gorm:"type:varchar(255)"cms_post.go:26);关联表 CmsRelateCategory.CategoryCmsRelateTags.TagsforeignKey 写反(cms_relate_category.go:12cms_relate_tags.go:13),导致 Preload("Categories.Category")/Preload("Tags.Tags") 关联数据取不到(见 6.2)。

cms_post(经 GORM 自动迁移创建)

字段 类型 键/约束 说明
id uint PK 自增主键
identity varchar(36) index 业务唯一标识UUID
owner_id/owner_identity - - 所有者(来自 Std_Owner
site_identity varchar(36) index 站点标识
types int32 gorm:"type:varchar(255)" 文章类型tag 与字段类型矛盾)
title varchar(255) - 标题
hash varchar(255) uniqueIndex URL 唯一键
content/description/cover_path/target_url/source_url text/varchar - 内容/描述/封面/外链
author/author_identity varchar - 作者
hits/like_hits/unlike_hits/comment_hits int64 default 0 统计计数
has_accessory bool - 是否有附件
categories/tags/accessories 关联 - 关联 CmsRelateCategory/CmsRelateTags/CmsAccessory

cms_comment

字段 类型 键/约束 说明
id uint PKgorm.Model -
identity - (来自 Std_Identity 业务标识
post_identity/owner_identity/owner_name/role varchar index 归属/作者
parent_id uint index 父评论 id0 为顶级)
reply_identity varchar(36) index 被回复评论
Cms text - 评论内容
hits/like_hits/unlike_hits/comment_hits int64 default 0 计数

cms_category / cms_tags / cms_pages / cms_site

关键字段 说明
cms_category category_key uniqueIndex、title not null、parent_id index、Children 自关联 层级分类
cms_tags title not null、cover_pathintro 标签(无 uniqueKey 约束)
cms_pages key uniqueIndex、title/content 默认空串、hits 计数、Tags/Accessories 关联 单页
cms_site title/description/icon_path/keywords/domain/seo/theme/configsnot null 站点配置(无任何写入路径

关联表 cms_relate_category / cms_relate_tags / cms_accessory

关键字段 说明
cms_relate_category identity uniqueIndex、post_identity index、category_identity index、Category 关联 文章-分类foreignKey 写反)
cms_relate_tags post_identity/pages_identity/tags_identity index、Tags 关联 文章/页面-标签foreignKey 写反)
cms_accessory post_id/post_identity/pages_id/pages_identityfile_path not null 附件

5. 核心流程

flowchart TD
    A["POST /cms.Post/IncrPostLike"] --> B["ParseMetaCtx 校验登录态"]
    B --> C["校验 op_identity / post_identity 非空"]
    C --> D["IncrOrDescPostField: UPDATE cms_post SET like_hits = like_hits + 1 WHERE identity=?"]
    D --> E["返回 OK"]
flowchart TD
    A["POST /cms.Post/AddComment"] --> B["ParseMetaCtx 取 auth"]
    B --> C["impl.DBService.Table('mall_staff').Take(&authName, 'identity=?', auth.Identity)"]
    C --> D{"命中 mall_staff 行?"}
    D -- "否 / name 为 NULL" --> P["authName['name'].(string) 触发 panic → 500"]
    D -- "是" --> E["事务: 写 cms_comment + post.comment_hits +1 + 父评论 comment_hits +1"]
    E --> F["返回 comment identity"]

6. 审计发现

6.1 安全

级别 位置 问题
internal/logic/post/delete.go:29modify.go:15-22delete_comment.go:24modify_comment.go:33 越权删除/修改:文章 Delete/Modify、评论 DeleteComment/ModifyComment 只校验"是否登录",不校验调用者是否为资源 owner/作者。DeletePost(in.Identity, "") 第二参 authorIdentity 恒为空且函数体未使用(query.go:207ModifyComment 直接 Where("identity=?", in.Identity).Updates(data)。任意登录用户可删改他人文章/评论。
internal/logic/post/search.go:31-34 无分页上限 / 可拖全表if pageSize < 50 { pageSize = 50 } 把小于 50 的请求强制放大到 50完全没有 page_size > 50 的上限。客户端传 page_size=1000000 即可一次拉走整张 cms_post(含 content 大字段造成全表返回与读取放大DoS。proto 注释明确"最多 50",实现与之相反。
internal/logic/post/comment_list.go:24-27 CommentListParseMetaCtx 被整段注释,评论列表接口对匿名开放,可遍历任意文章全部评论与作者信息。
internal/logic/post/incr_post_like.go:38incr_comment_like.go:29 等 4+4 处 赞/踩无去重、可刷量op_identity 仅做"非空"校验,从不记录"谁点过",同一用户可重复 IncrPostLike 无限累加;Desc* 路径可把计数刷成负数(无 floor 0 约束)。模型与计数仅用 int64CHECK
internal/logic/tags/create.go:28-30 标签重复创建走 First 命中即返回 errcode.ErrDB(不是"已存在"语义),且 dedup 依赖"先查后插"非原子,存在 TOCTOU 竞态。

6.2 正确性与逻辑缺陷

级别 位置 问题
internal/logic/post/add_comment.go:35-40 取 mall_staff 名字 panicTake(&authName, "identity=?", auth.Identity) 若当前用户不在 mall_staff 表(如普通会员/客户),Take 返回 ErrRecordNotFound,紧随其后的 authName["name"].(string) 对缺失 key 取到 nil 触发 panic(服务端 500且 gRPC 默认无 recover 会中断该请求链路)。即便命中,name 列为 NULL 同样 panic。
internal/models/query.go:303-334DeleteComment+ 277-297AddComment 评论计数不一致AddComment每条评论(含子评论)都会让 post.comment_hits +1;而 DeleteComment 删除父评论时只做 post.comment_hits -1,再级联删子评论(parent_id = comment.ID)。当父评论下有 N 个子评论被一并删除时,文章评论数只减 1实际应减 N+1 → 计数长期偏离真实值。
internal/logic/pages/get_by_identity.go:21,76 页面点击量不增GetPages("Cms_pages.identity", in.Identity) 查询用 identity,但计数更新写成 Where("key = ?", val)val 是 identity 值而非 key),匹配不到行,hits 永远不 +1。仅 GetByKey 路径(GetPages("key", in.Key))计数正确。
internal/models/cms_relate_category.go:12cms_relate_tags.go:13 Preload 外键写反Category CmsCategory gorm:"foreignKey:Identity;references:CategoryIdentity" 实际生成 cms_category.category_identity = cms_relate_category.identity(应为 cms_category.identity = cms_relate_category.category_identityTags 同理。导致 Preload("Categories.Category")/Preload("Tags.Tags")query.go:136,242post/fetch.go:52-72get_by_identity.go:68-79pages/fetch.go:52-62)取到的分类/标签对象为空或错位,列表与详情的分类/标签数据失真。
internal/models/query.go:162-169 修改文章时附件路径被覆盖ModifyPostaccessory 参数实际接收的是 AccessoryIdentityArraypost/modify.go:43),却把每个字符串直接赋给 CmsAccessory.FilePathFilePath: v)。即修改文章后,附件记录的文件路径会被写成"附件 identity 字符串",原 file_path 丢失。
internal/logic/pages/modify.go:33 vs create.go:46 页面 key 不一致Createutils.FormatKey(in.Key) 格式化,Modify 直接 Key: in.Key 不做格式化,二者对同名 key 可能写入不同值,破坏 uniqueIndex 一致性。
internal/logic/tags/create.go:19,28-33 死代码导致语义错误var cnt int64 = 0 声明后从未赋值,下方 if cnt > 0 { return ... ErrAlreadyExists } 永远为假;重复标签判定实际依赖 Firsterr == nil,但返回的是 errcode.ErrDB 而非"已存在"。
internal/models/cms_post.go:26 Types int32 但 gorm type:varchar(255)PostListcms_post.types = ?int比较 varchar 列(query.go:130),类型语义错位,依赖数据库隐式转换。
internal/logic/post/modify.go:38post/delete.go:30 错误透传不一致:ModifyPost/DeletePost 失败统一返回 errcode.ErrDB,但 modify_comment.go:34 把原始 err 直接返回,错误体格式不统一。

6.3 未完成实现

级别 位置 问题
internal/logic/site/{create,modify,delete,fetch,get}.go Site 全系列占位Fetch/GetTODO 后返回空(site/fetch.go:18,20site/get.go:24Create/Modify/Delete 解析登录态后直接 return StatusReply{OK} 不落库/不删除(site/create.go:20-22site/modify.go:21-23site/delete.go:27。proto 与 server 已对外暴露 cms.Site,调用方会拿到"成功但无数据/数据不变"的假象。cms_site 表已迁移却无任何写入路径。
internal/models/query.go:346 评论点赞明确留 // TODO: 记录点赞对象避免重复点赞,去重能力长期缺失(与 6.1 赞踩刷量同一根因)。
internal/logic/post/delete.go:29、各 Modify/Delete// Todo: 注释 归属校验、作者校验多处以 // Todo: 留空,未落地。

6.4 健壮性与可维护性

级别 位置 问题
internal/models/query.go:119category/fetch.go:20pages/get_by_identity.go:71post/comment_list.go:44 多处 .Debug() 打开 SQL 全量日志,生产环境会刷屏并可能泄露数据内容。
internal/logic/post/comment_list.go:30category/fetch.go:21tags/fetch.go:21 调试用 fmt.Println("err = ", ...)/fmt.Println("in = ", in) 残留,泄漏请求参数到标准输出。
internal/logic/post/ext.gopages/ext.go fmtKey 函数定义但从未被调用create/modify 用的是 utils.FormatKey),属死代码。
internal/logic/tags/delete.go:24 删除标签时不清理 cms_relate_tags,遗留孤儿关联(文章/页面仍指向已删标签)。
internal/logic/category/delete.go:23query.go:370-389 DeleteCategory 仅禁止"有子分类"的删除,未检查分类是否仍被 cms_relate_category 引用,删除被引用的分类会留下孤儿关联。
全模块 无任何 *_test.go 单元测试36 个 .http 仅为手工调用示例。上述 panic、计数不一致、分页越界等问题若有单测可早期暴露。
internal/config/config.go:49 conf.NotNil(Spec.Service, Spec.Cache) 未校验 Databases;但 impl.go:43with.Databases 多数实现在配置缺失时返回 nil首个查询即空指针pkgs/all 已注入共享 DB 故当前不触发,独立运行时风险高)。

7. 风险汇总

编号 级别 问题 影响面
C1 文章/评论 增/删/改 不校验归属(越权) 数据完整性、内容安全
C2 AddComment 未命中 mall_staffauthName["name"].(string) panic 接口可用性500/链路中断)
C3 Searchpage_size 上限(可拖全表) 性能/DoS、数据泄露
C4 删除父评论时 post.comment_hits 计数不一致 统计数据失真
C5 Site 全系列占位空实现(声明未实现) 功能缺失、调用方误判成功
C6 赞/踩无去重且可刷成负数 互动数据可信性、刷量
C7 GetByIdentity(页面) 点击量不增 统计失真
C8 CommentList 鉴权被注释(匿名可读全部评论) 信息泄露
C9 关联表 Preload 外键写反,分类/标签数据取不到 列表/详情数据错误
C10 ModifyPost 附件路径被写成 identity 字符串 附件数据损坏
C11 页面 Modify 的 key 未格式化,与 Create 不一致 唯一键语义混乱
C12 Tags.Create 死代码 cnt + dedup 返回 ErrDB 重复标签处理语义错误
C13 多处 .Debug() + fmt.Println 残留 日志泄露、性能
C14 标签/分类删除遗留孤儿关联 数据一致性
C15 无单测、死代码 fmtKey 可维护性
C16 config.NotNil 未校验 Databases 独立运行崩溃风险

8. 修复建议(务实项)

  1. 越权C1:在 post.Delete/post.Modify/post.DeleteComment/post.ModifyComment 中,先按 identity 取资源,校验 owner_identity/author_identityParseMetaCtx 返回的登录身份一致(或调用方具备管理角色)再执行;DeletePost 应接收并比较 authorIdentity。不要新增抽象层,仅在现有函数开头加一段归属判定。
  2. 评论 panicC2add_comment.go:35-40 不要用 map[string]any + 类型断言取 name。改为先用 Select("name")mall_staff,对 ErrRecordNotFound/NULL 给出兜底(如空串或调用方已下发的昵称),删除 .(string) 断言。
  3. 分页上限C3search.go:31 改为"若 page_size<=0 默认 10page_size>50 截断为 50",与 Fetch 的既有逻辑(post/fetch.go:18-23)对齐;同时给 pages.Fetch/tags.Fetch 等补上分页(当前 tags 列表无分页)。
  4. 计数一致性C4DeleteComment 级联删除子评论时,按"被删评论及子评论总条数"一次性 post.comment_hits - N;或在删除前 Count 子评论数量后再减,避免只减 1。
  5. 页面点击量C7get_pages.go:76Where("key = ?", val) 改为 Where("identity = ?", val)(与查询 key 一致);或统一用 in.Identity 作为更新条件,与 GetPostquery.go:248)保持一致写法。
  6. Preload 外键C9:把 CmsRelateCategory.CategoryCmsRelateTags.Tags 的 gorm tag 改为 foreignKey:CategoryIdentity;references:Identity / foreignKey:TagsIdentity;references:Identity,使 Preload 正确取到分类/标签对象。
  7. 赞/踩去重C6对应 TODO query.go:346:在 IncrOrDescPostField/IncrOrDescCommentField 前,先查"该 op_identity 是否已对目标点过赞"的记录(新增轻量关系表或唯一索引),已点过则忽略/抵消;并对 desc 路径加 WHERE col > 0 约束防止负数。
  8. Site 占位C5:要么补齐 site/* 五个方法的真实读写逻辑(建/改/删走 models.CmsSite、列表/详情查询),要么在 proto 与 server 中移除该服务以免对外暴露假成功;当前不应让其返回"OK 但无副作用"。
  9. 清理C13:删除 comment_list.go:30category/fetch.go:21tags/fetch.go:21fmt.Println,以及 query.go/get_pages.go/comment_list.go/category/fetch.go.Debug();删除未使用的 fmtKeypost/ext.gopages/ext.go)。
  10. 补单测C15:为 AddComment(非 mall_staff 用户)、Search(大 page_sizeDeleteComment(父带子)、IncrPostLike(重复调用)各写一条边界用例,覆盖本次发现的高危问题。

本报告只列出与现有实现直接相关的修复项不引入统一框架、DTO/VO 层、DDD/CQRS 等额外抽象。所有结论均附 文件:行号 证据;未发现证据处已标注【信息不足】或明确为推断。

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属本机既有问题

编号 级别 问题 处理结果
C1 文章/评论 增/删/改 不校验归属(越权) 已修复:post 系列(文章与评论,模型含 AuthorIdentity/OwnerIdentity)按 auth.Identity 限定,非作者/非归属者返回 ErrPermissionDeniedpages 系列(CmsPages 单页文章)当前模型只有 types.Std_IICUDS + 站点/内容字段,没有任何作者或归属列,在现有表结构下无法判定归属——需要新增归属列才能实施,属契约/表结构限制,本轮未擅自改表
C2 AddComment 未命中 mall_staff 时类型断言 panic 已修复:改为带 ok 的安全类型断言并给默认值
C3 Searchpage_size 上限 已修复:归一化分页(page 下限 1、page_size 上限 100
C4 删除父评论导致 comment_hits 计数不一致 已修复:删除时同步修正计数
C5 Site 全系列占位空实现却返回成功 已修复:改为显式返回 codes.Unimplemented,不再假成功
C6 赞/踩无去重且可刷成负数 已修复:计数改为条件更新并加下限保护,不再出现负值。完整去重需要「谁点过」的记录表/字段,属契约外,未做

未纳入本轮范围

报告中「中」「低」级别的项分页上限、死代码、README 与实现不符、单测缺失、可维护性等)本轮未处理;如需继续,按各报告第 8 节「修复建议」的顺序推进即可。

本轮整改未修改任何 proto/*.protopb/*.go,因此少数需要新增接口字段才能完整实现的项目(已在处理结果中标注)做了安全降级。