23 KiB
cms(站点内容管理)代码审计报告
| 项 | 内容 |
|---|---|
| 审计对象 | module/base/cms |
| 服务域 | 基础与平台服务(内容管理:文章 / 页面 / 分类 / 标签 / 评论 / 赞踩) |
| 审计日期 | 2026-09-22 |
| 代码规模 | 手写 Go 文件约 62 个(logic 41、models 10、server 6、service 2、config 1、impl 1、utils 1、cmd 1);pb 生成代码 17 个(post/pages/tags/site/category 三件套 + const + blocks_compat);proto 6 个;.http 测试 36 个;无 cmd/main、无 yaml 配置、无 *_test.go 单测 |
| 入口 | 仅聚合入口 pkgs/all、pkgs/ecmall(经 service.Expose 注册 gRPC + HTTP Gateway);cmd/cli/main.go 仅为 Hello World 占位,无独立可运行进程 |
| 对外协议 | gRPC(cms.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 |
Dependencies(Redis/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 |
对照 proto(
post.proto/pages.proto/category.proto/tags.proto/site.proto)与internal/server/*_server.go+service/expose.go,5 个服务的所有 rpc 均已在 gRPC 与 Gateway 注册;不存在"声明但未注册"的方法。但Site全部 5 个方法为"已注册、无逻辑"的占位实现(代码内含// TODO: add your logic code & delete this line.),且cms_site表已通过init()纳入自动迁移,却无任何写入路径——属于"声明但未实现"。
4. 数据模型与表
模型 gorm tag 存在反直觉之处:
cms_post.types为int32但gorm:"type:varchar(255)"(cms_post.go:26);关联表CmsRelateCategory.Category、CmsRelateTags.Tags的foreignKey写反(cms_relate_category.go:12、cms_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 | PK(gorm.Model) |
- |
identity |
- | (来自 Std_Identity) |
业务标识 |
post_identity/owner_identity/owner_name/role |
varchar | index | 归属/作者 |
parent_id |
uint | index | 父评论 id(0 为顶级) |
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_path、intro |
标签(无 uniqueKey 约束) |
cms_pages |
key uniqueIndex、title/content 默认空串、hits 计数、Tags/Accessories 关联 |
单页 |
cms_site |
title/description/icon_path/keywords/domain/seo/theme/configs 均 not 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_identity、file_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:29、modify.go:15-22、delete_comment.go:24、modify_comment.go:33 |
越权删除/修改:文章 Delete/Modify、评论 DeleteComment/ModifyComment 只校验"是否登录",不校验调用者是否为资源 owner/作者。DeletePost(in.Identity, "") 第二参 authorIdentity 恒为空且函数体未使用(query.go:207),ModifyComment 直接 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 |
CommentList 的 ParseMetaCtx 被整段注释,评论列表接口对匿名开放,可遍历任意文章全部评论与作者信息。 |
| 中 | internal/logic/post/incr_post_like.go:38、incr_comment_like.go:29 等 4+4 处 |
赞/踩无去重、可刷量:op_identity 仅做"非空"校验,从不记录"谁点过",同一用户可重复 IncrPostLike 无限累加;Desc* 路径可把计数刷成负数(无 floor 0 约束)。模型与计数仅用 int64 无 CHECK。 |
| 低 | internal/logic/tags/create.go:28-30 |
标签重复创建走 First 命中即返回 errcode.ErrDB(不是"已存在"语义),且 dedup 依赖"先查后插"非原子,存在 TOCTOU 竞态。 |
6.2 正确性与逻辑缺陷
| 级别 | 位置 | 问题 |
|---|---|---|
| 高 | internal/logic/post/add_comment.go:35-40 |
取 mall_staff 名字 panic:Take(&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-334(DeleteComment)+ 277-297(AddComment) |
评论计数不一致: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:12、cms_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_identity);Tags 同理。导致 Preload("Categories.Category")/Preload("Tags.Tags")(query.go:136,242、post/fetch.go:52-72、get_by_identity.go:68-79、pages/fetch.go:52-62)取到的分类/标签对象为空或错位,列表与详情的分类/标签数据失真。 |
| 中 | internal/models/query.go:162-169 |
修改文章时附件路径被覆盖:ModifyPost 的 accessory 参数实际接收的是 AccessoryIdentityArray(post/modify.go:43),却把每个字符串直接赋给 CmsAccessory.FilePath(FilePath: v)。即修改文章后,附件记录的文件路径会被写成"附件 identity 字符串",原 file_path 丢失。 |
| 中 | internal/logic/pages/modify.go:33 vs create.go:46 |
页面 key 不一致:Create 用 utils.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 } 永远为假;重复标签判定实际依赖 First 的 err == nil,但返回的是 errcode.ErrDB 而非"已存在"。 |
| 低 | internal/models/cms_post.go:26 |
Types int32 但 gorm type:varchar(255);PostList 用 cms_post.types = ?(int)比较 varchar 列(query.go:130),类型语义错位,依赖数据库隐式转换。 |
| 低 | internal/logic/post/modify.go:38、post/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/Get 仅 TODO 后返回空(site/fetch.go:18,20、site/get.go:24);Create/Modify/Delete 解析登录态后直接 return StatusReply{OK} 不落库/不删除(site/create.go:20-22、site/modify.go:21-23、site/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:119、category/fetch.go:20、pages/get_by_identity.go:71、post/comment_list.go:44 |
多处 .Debug() 打开 SQL 全量日志,生产环境会刷屏并可能泄露数据内容。 |
| 低 | internal/logic/post/comment_list.go:30、category/fetch.go:21、tags/fetch.go:21 |
调试用 fmt.Println("err = ", ...)/fmt.Println("in = ", in) 残留,泄漏请求参数到标准输出。 |
| 低 | internal/logic/post/ext.go、pages/ext.go |
fmtKey 函数定义但从未被调用(create/modify 用的是 utils.FormatKey),属死代码。 |
| 低 | internal/logic/tags/delete.go:24 |
删除标签时不清理 cms_relate_tags,遗留孤儿关联(文章/页面仍指向已删标签)。 |
| 低 | internal/logic/category/delete.go:23 → query.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:43 的 with.Databases 多数实现在配置缺失时返回 nil,首个查询即空指针(pkgs/all 已注入共享 DB 故当前不触发,独立运行时风险高)。 |
7. 风险汇总
| 编号 | 级别 | 问题 | 影响面 |
|---|---|---|---|
| C1 | 高 | 文章/评论 增/删/改 不校验归属(越权) | 数据完整性、内容安全 |
| C2 | 高 | AddComment 未命中 mall_staff 时 authName["name"].(string) panic |
接口可用性(500/链路中断) |
| C3 | 高 | Search 无 page_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. 修复建议(务实项)
- 越权(C1):在
post.Delete/post.Modify/post.DeleteComment/post.ModifyComment中,先按 identity 取资源,校验owner_identity/author_identity与ParseMetaCtx返回的登录身份一致(或调用方具备管理角色)再执行;DeletePost应接收并比较authorIdentity。不要新增抽象层,仅在现有函数开头加一段归属判定。 - 评论 panic(C2):
add_comment.go:35-40不要用map[string]any+ 类型断言取name。改为先用Select("name")查mall_staff,对ErrRecordNotFound/NULL 给出兜底(如空串或调用方已下发的昵称),删除.(string)断言。 - 分页上限(C3):
search.go:31改为"若page_size<=0默认 10,若page_size>50截断为 50",与Fetch的既有逻辑(post/fetch.go:18-23)对齐;同时给pages.Fetch/tags.Fetch等补上分页(当前 tags 列表无分页)。 - 计数一致性(C4):
DeleteComment级联删除子评论时,按"被删评论及子评论总条数"一次性post.comment_hits - N;或在删除前Count子评论数量后再减,避免只减 1。 - 页面点击量(C7):
get_pages.go:76的Where("key = ?", val)改为Where("identity = ?", val)(与查询 key 一致);或统一用in.Identity作为更新条件,与GetPost(query.go:248)保持一致写法。 - Preload 外键(C9):把
CmsRelateCategory.Category、CmsRelateTags.Tags的 gorm tag 改为foreignKey:CategoryIdentity;references:Identity/foreignKey:TagsIdentity;references:Identity,使Preload正确取到分类/标签对象。 - 赞/踩去重(C6,对应 TODO
query.go:346):在IncrOrDescPostField/IncrOrDescCommentField前,先查"该 op_identity 是否已对目标点过赞"的记录(新增轻量关系表或唯一索引),已点过则忽略/抵消;并对desc路径加WHERE col > 0约束防止负数。 - Site 占位(C5):要么补齐
site/*五个方法的真实读写逻辑(建/改/删走models.CmsSite、列表/详情查询),要么在 proto 与 server 中移除该服务以免对外暴露假成功;当前不应让其返回"OK 但无副作用"。 - 清理(C13):删除
comment_list.go:30、category/fetch.go:21、tags/fetch.go:21的fmt.Println,以及query.go/get_pages.go/comment_list.go/category/fetch.go的.Debug();删除未使用的fmtKey(post/ext.go、pages/ext.go)。 - 补单测(C15):为
AddComment(非 mall_staff 用户)、Search(大 page_size)、DeleteComment(父带子)、IncrPostLike(重复调用)各写一条边界用例,覆盖本次发现的高危问题。
本报告只列出与现有实现直接相关的修复项,不引入统一框架、DTO/VO 层、DDD/CQRS 等额外抽象。所有结论均附
文件:行号证据;未发现证据处已标注【信息不足】或明确为推断。