cloud(个人云空间与资料管理)代码审计报告
| 项 |
内容 |
| 审计对象 |
module/base/cloud |
| 服务域 |
基础与平台服务(个人云空间:网盘 / 相册 / 笔记 / 收藏 / 私人资料 / 分享 / 空间统计) |
| 审计日期 |
2026-09-22 |
| 代码规模 |
Go 文件约 80 个(全部手写:7 个 server 注册、59 个 logic 实现、11 个 models、config/impl/cmd 各 1);proto 9 个、etc yaml 3 个、README 1 个;生成代码 pb/*.go 仅看接口签名 |
| 入口 |
无独立 cmd/main(仅 cmd/cli/main.go 为 "Hello World" 桩);作为 gRPC 服务经聚合入口 pkgs/all、pkgs/ecmall 接入 |
| 对外协议 |
gRPC(7 个 service)+ HTTP Gateway(grpc-gateway 默认路由 /cloud.<Service>/<Method>,proto 无 google.api.http 注解) |
| 结论摘要 |
接口全集已实现、均以 service.ParseMetaCtx 做网关鉴权、并以 passport_id = auth.ID 做归属隔离(无跨用户越权读)。但存在私人资料"加密"未真正落地(明文存储、密钥由客户端传入、加解密为无状态工具不落库)、空间默认 key_identifier="default" 与全局唯一索引冲突导致多用户仅首名可用、GetDir/GetDirTree 对根目录 ParentID 空指针解引用 panic、容量配额仅统计不限制、**分享链路断裂(收件人无法取得他人资源、CreateShare 不校验资源归属)**等严重缺陷。 |
1. 服务定位与职责
个人云空间聚合服务,包含七个子域:云盘目录/文件(disk)、相册/照片(album)、笔记/附件(note)、书签收藏(bookmark)、私人加密资料(private)、分享(share)、空间统计(space)。每个子域通过 gRPC 暴露 CRUD 与列表/搜索接口,由 HTTP Gateway 透出 REST。多用户数据隔离通过 passport_id(网关注入的身份)实现,所有归属校验在 logic 层以 WHERE passport_id = ? 完成。
2. 代码结构与入口
| 路径 |
职责 |
cmd/cli/main.go |
CLI 桩,仅打印 "Hello World",无实质逻辑 |
internal/config/config.go |
配置结构(Base、Databases、MicroService、Rpc、Gateway、Apm、Etcd)与校验 |
internal/impl/impl.go |
初始化 Redis、Etcd、DB、Memory 等共享实例 |
internal/server/*.go |
7 个 gRPC server 注册(Disk/Album/Bookmark/Note/Private/Share/Space),逐方法转发到 logic |
internal/logic/disk/* |
目录树、文件上传/移动/复制/搜索(15 个实现) |
internal/logic/album/* |
相册与照片(12 个实现) |
internal/logic/note/* |
笔记与附件(10 个实现) |
internal/logic/bookmark/* |
书签与导入(6 个实现) |
internal/logic/private/* |
私人资料与加解密(9 个实现) |
internal/logic/share/* |
分享与密码校验(5 个实现) |
internal/logic/space/* |
空间统计(2 个实现) |
internal/models/*.go |
11 个 GORM 模型 + query.go(CloudBase 内嵌结构) |
proto/*.proto |
7 个 service 定义 + const.proto 公共消息 |
etc/cloud_dev.yaml 等 |
运行配置(Gateway 开启、MicroService.Anonymous 含失效项 cloud.ping.hello) |
依赖注入与 logs 模块不同:本模块通过 service.ParseMetaCtx(ctx, nil) 从网关注入的 metadata 解析 auth.ID/Identity,每个 logic 函数首行即调用并校验,缺失即返回错误,因此所有接口均在网关层强制鉴权。
3. 接口清单
鉴权列统一说明:全部接口经网关鉴权(ParseMetaCtx 缺失即报错)。除特别标注外,归属校验均为 WHERE passport_id = auth.ID。REST 路径为 grpc-gateway 默认格式 /cloud.<Service>/<Method>(proto 未定义 http 注解)。
声明但未实现 / 占位:本次审计中,全部 59 个 proto 方法均有对应 logic 实现,未发现 TODO 占位。但"私人资料加密存储"与"分享向他人交付资源"两块在逻辑上未真正闭合(见 6.2/6.3)。
Disk(15)
| 方法 |
REST 路径 |
功能 |
鉴权/归属 |
实现位置 |
| CreateDir |
/cloud.Disk/CreateDir |
建目录(校验父目录归属) |
passport 归属 |
logic/disk/create_dir.go:20 |
| GetDir |
/cloud.Disk/GetDir |
目录详情(含子目录/文件) |
passport 归属 |
logic/disk/get_dir.go:16 |
| UpdateDir |
/cloud.Disk/UpdateDir |
改名/重算路径 |
passport 归属 |
logic/disk/update_dir.go:19 |
| DeleteDir |
/cloud.Disk/DeleteDir |
删目录(空目录才允许) |
passport 归属 |
logic/disk/delete_dir.go:17 |
| ListDirs |
/cloud.Disk/ListDirs |
分页列表 |
passport 归属 |
logic/disk/list_dirs.go:14 |
| GetDirTree |
/cloud.Disk/GetDirTree |
目录树 |
passport 归属 |
logic/disk/get_dir_tree.go:16 |
| MoveDir |
/cloud.Disk/MoveDir |
移动目录 |
passport 归属 |
logic/disk/move_dir.go:19 |
| UploadFile |
/cloud.Disk/UploadFile |
上传文件(校验目录归属) |
passport 归属 |
logic/disk/upload_file.go:19 |
| GetFile |
/cloud.Disk/GetFile |
文件详情(JOIN dir) |
passport 归属 |
logic/disk/get_file.go:16 |
| UpdateFile |
/cloud.Disk/UpdateFile |
改文件名等 |
passport 归属 |
logic/disk/update_file.go:18 |
| DeleteFile |
/cloud.Disk/DeleteFile |
删文件 |
passport 归属 |
logic/disk/delete_file.go:17 |
| ListFiles |
/cloud.Disk/ListFiles |
分页列表(JOIN dir) |
passport 归属 |
logic/disk/list_files.go:14 |
| MoveFile |
/cloud.Disk/MoveFile |
移动文件 |
passport 归属 |
logic/disk/move_file.go:17 |
| CopyFile |
/cloud.Disk/CopyFile |
复制文件 |
passport 归属 |
logic/disk/copy_file.go:19 |
| SearchFiles |
/cloud.Disk/SearchFiles |
关键字搜索 |
passport 归属 |
logic/disk/search_files.go:15 |
Album(12)
| 方法 |
REST 路径 |
功能 |
鉴权/归属 |
实现位置 |
| CreateAlbum |
/cloud.Album/CreateAlbum |
建相册 |
passport 归属 |
logic/album/create_album.go:19 |
| GetAlbum |
/cloud.Album/GetAlbum |
相册详情(含照片) |
passport 归属 |
logic/album/get_album.go:16 |
| UpdateAlbum |
/cloud.Album/UpdateAlbum |
改相册 |
passport 归属 |
logic/album/update_album.go:18 |
| DeleteAlbum |
/cloud.Album/DeleteAlbum |
删相册(先删照片) |
passport 归属 |
logic/album/delete_album.go:17 |
| ListAlbums |
/cloud.Album/ListAlbums |
分页列表(含照片) |
passport 归属 |
logic/album/list_albums.go:14 |
| SetCoverPhoto |
/cloud.Album/SetCoverPhoto |
设封面(校验照片归属相册) |
passport 归属 |
logic/album/set_cover_photo.go:17 |
| UploadPhoto |
/cloud.Album/UploadPhoto |
传照片(校验相册归属) |
passport 归属 |
logic/album/upload_photo.go:19 |
| GetPhoto |
/cloud.Album/GetPhoto |
照片详情(JOIN album) |
passport 归属 |
logic/album/get_photo.go:16 |
| UpdatePhoto |
/cloud.Album/UpdatePhoto |
改照片 |
passport 归属 |
logic/album/update_photo.go:17 |
| DeletePhoto |
/cloud.Album/DeletePhoto |
删照片 |
passport 归属 |
logic/album/delete_photo.go:17 |
| ListPhotos |
/cloud.Album/ListPhotos |
分页列表 |
passport 归属 |
logic/album/list_photos.go:14 |
| MovePhoto |
/cloud.Album/MovePhoto |
移动照片到其他相册 |
passport 归属 |
logic/album/move_photo.go:17 |
Bookmark(6)
| 方法 |
REST 路径 |
功能 |
鉴权/归属 |
实现位置 |
| CreateBookmark |
/cloud.Bookmark/CreateBookmark |
建书签 |
passport 归属 |
logic/bookmark/create_bookmark.go:19 |
| GetBookmark |
/cloud.Bookmark/GetBookmark |
书签详情(仅 id) |
passport 归属(id+passport) |
logic/bookmark/get_bookmark.go:16 |
| UpdateBookmark |
/cloud.Bookmark/UpdateBookmark |
改书签 |
passport 归属 |
logic/bookmark/update_bookmark.go:18 |
| DeleteBookmark |
/cloud.Bookmark/DeleteBookmark |
删书签(仅 id) |
passport 归属(id+passport) |
logic/bookmark/delete_bookmark.go:17 |
| ListBookmarks |
/cloud.Bookmark/ListBookmarks |
分页列表 |
passport 归属 |
logic/bookmark/list_bookmarks.go:14 |
| ImportBookmarks |
/cloud.Bookmark/ImportBookmarks |
批量导入 |
passport 归属 |
logic/bookmark/import_bookmarks.go:21 |
Note(10)
| 方法 |
REST 路径 |
功能 |
鉴权/归属 |
实现位置 |
| CreateNote |
/cloud.Note/CreateNote |
建笔记 |
passport 归属 |
logic/note/create_note.go:19 |
| GetNote |
/cloud.Note/GetNote |
笔记详情(含附件) |
passport 归属 |
logic/note/get_note.go:16 |
| UpdateNote |
/cloud.Note/UpdateNote |
改笔记 |
passport 归属 |
logic/note/update_note.go:18 |
| DeleteNote |
/cloud.Note/DeleteNote |
删笔记(先删附件) |
passport 归属 |
logic/note/delete_note.go:17 |
| ListNotes |
/cloud.Note/ListNotes |
分页列表 |
passport 归属 |
logic/note/list_notes.go:14 |
| TogglePin |
/cloud.Note/TogglePin |
置顶切换 |
passport 归属 |
logic/note/toggle_pin.go:17 |
| IncrementViews |
/cloud.Note/IncrementViews |
浏览+1 |
passport 归属(仅本人) |
logic/note/increment_views.go:17 |
| SearchNotes |
/cloud.Note/SearchNotes |
关键字搜索 |
passport 归属 |
logic/note/search_notes.go:15 |
| InsertAttachment |
/cloud.Note/InsertAttachment |
加附件 |
passport 归属 |
logic/note/insert_attachment.go:17 |
| DeleteAttachment |
/cloud.Note/DeleteAttachment |
删附件(JOIN note) |
passport 归属 |
logic/note/delete_attachment.go:17 |
Private(9)
| 方法 |
REST 路径 |
功能 |
鉴权/归属 |
实现位置 |
| CreatePrivateData |
/cloud.Private/CreatePrivateData |
建私人资料 |
passport 归属 |
logic/private/create_private_data.go:19 |
| GetPrivateData |
/cloud.Private/GetPrivateData |
详情 |
passport 归属 |
logic/private/get_private_data.go:16 |
| UpdatePrivateData |
/cloud.Private/UpdatePrivateData |
改资料 |
passport 归属 |
logic/private/update_private_data.go:18 |
| DeletePrivateData |
/cloud.Private/DeletePrivateData |
删资料 |
passport 归属 |
logic/private/delete_private_data.go:17 |
| ListPrivateData |
/cloud.Private/ListPrivateData |
分页列表 |
passport 归属 |
logic/private/list_private_data.go:14 |
| GetPrivateDataByType |
/cloud.Private/GetPrivateDataByType |
按类型查 |
passport 归属 |
logic/private/get_private_data_by_type.go:16 |
| SearchPrivateData |
/cloud.Private/SearchPrivateData |
搜索 |
passport 归属 |
logic/private/search_private_data.go:15 |
| EncryptData |
/cloud.Private/EncryptData |
AES 加密(无状态工具) |
仅网关鉴权,auth 被丢弃、不落库 |
logic/private/encrypt_data.go:19 |
| DecryptData |
/cloud.Private/DecryptData |
AES 解密(无状态工具) |
仅网关鉴权,auth 被丢弃、不落库 |
logic/private/decrypt_data.go:17 |
Share(5)
| 方法 |
REST 路径 |
功能 |
鉴权/归属 |
实现位置 |
| CreateShare |
/cloud.Share/CreateShare |
建分享 |
passport 归属(不校验 resource_id 归属) |
logic/share/create_share.go:19 |
| GetShare |
/cloud.Share/GetShare |
分享详情 |
passport 归属(仅创建者可见) |
logic/share/get_share.go:16 |
| DeleteShare |
/cloud.Share/DeleteShare |
删分享 |
passport 归属 |
logic/share/delete_share.go:17 |
| ListShares |
/cloud.Share/ListShares |
列表 |
passport 归属 |
logic/share/list_shares.go:14 |
| ValidateSharePassword |
/cloud.Share/ValidateSharePassword |
校验密码 |
仅网关鉴权;按 identity 查、无 passport 限制、不校验 is_public |
logic/share/validate_share_password.go:18 |
Space(2)
| 方法 |
REST 路径 |
功能 |
鉴权/归属 |
实现位置 |
| Get |
/cloud.Space/Get |
空间统计(缺则建默认) |
passport 归属 |
logic/space/get.go:16 |
| GetByKeyIdentifier |
/cloud.Space/GetByKeyIdentifier |
按 key 查空间 |
passport 归属 |
logic/space/get_by_key_identifier.go:16 |
4. 数据模型与表
公共内嵌:types.Std_IICUDS(id、created_at、updated_at、deleted_at)、types.Std_Passport(passport_id、passport_identity)、CloudBase(cloud_id、cloud_identity)。表名由 GORM 取 struct 蛇形复数。
表 cloud_disk_dir(云盘目录)
| 字段 |
类型 |
键/约束 |
说明 |
| id |
uint |
PK |
自增 |
| passport_id / passport_identity |
uint / varchar |
归属 |
来自 Std_Passport |
| cloud_id / cloud_identity |
uint / varchar(36) |
Index |
来自 CloudBase |
| parent_id |
*uint |
index, 可空 |
父目录;根目录为 nil |
| name |
varchar(100) |
- |
目录名 |
| path |
varchar(500) |
- |
完整路径 |
| created_at/updated_at/deleted_at |
- |
- |
时间戳 |
表 cloud_disk_file(云盘文件)
| 字段 |
类型 |
键/约束 |
说明 |
| id |
uint |
PK |
自增 |
| passport_id 等 |
- |
归属 |
Std_Passport |
| directory_id |
*uint |
index, 可空 |
所属目录 |
| name / original_name |
varchar(255) |
- |
文件名 |
| size |
int64 |
- |
字节 |
| mime_type |
varchar(100) |
- |
类型 |
| storage_path |
varchar(500) |
- |
实际存储路径 |
| hash |
varchar(64) |
- |
去重哈希 |
表 cloud_album(相册)
| 字段 |
类型 |
键/约束 |
说明 |
| id / passport_id 等 |
- |
PK/归属 |
- |
| name |
varchar(100) |
- |
名称 |
| description |
varchar(500) |
- |
描述 |
| cover_photo |
varchar(255) |
- |
封面 URL |
| is_private |
bool |
default false |
是否私有 |
表 cloud_photo(照片)
| 字段 |
类型 |
键/约束 |
说明 |
| id |
uint |
PK |
自增 |
| 无 passport 列 |
- |
- |
归属经 album 间接隔离 |
| album_id |
uint |
index |
所属相册 |
| title/description |
varchar(100)/(500) |
- |
- |
| file_path |
varchar(500) |
- |
路径 |
| file_size |
int64 |
- |
大小 |
| mime_type |
varchar(100) |
- |
类型 |
| width/height |
int |
- |
尺寸 |
| taken_at |
time |
- |
拍摄时间 |
| location |
varchar(200) |
- |
地点 |
| tags |
varchar(500) |
- |
标签 |
表 cloud_note(笔记)
| 字段 |
类型 |
键/约束 |
说明 |
| id / passport_id 等 |
- |
PK/归属 |
- |
| title |
varchar(200) |
- |
标题 |
| content |
text |
- |
内容 |
| category |
varchar(50) |
- |
分类 |
| tags |
varchar(500) |
- |
标签 |
| is_markdown |
bool |
default true |
- |
| is_pinned |
bool |
default false |
置顶 |
| is_private |
bool |
default false |
私有 |
| views |
int |
default 0 |
浏览数 |
表 note_attachments(笔记附件)
| 字段 |
类型 |
键/约束 |
说明 |
| id |
uint |
PK |
自增 |
| note_id |
uint |
index |
笔记 ID |
| file_name |
varchar(255) |
- |
文件名 |
| file_path |
varchar(500) |
- |
路径 |
| file_size |
int64 |
- |
大小 |
| mime_type |
varchar(100) |
- |
类型 |
| created_at |
time |
- |
创建时间 |
注:该表无 identity 列,故 DeleteAttachment 只能按 id 删除。
表 cloud_bookmark(书签)
| 字段 |
类型 |
键/约束 |
说明 |
| id / passport_id 等 |
- |
PK/归属 |
- |
| title |
varchar(200) |
- |
标题 |
| url |
varchar(500) |
- |
网址 |
| description/category |
varchar(500)/(50) |
- |
- |
| tags/icon |
varchar(500) |
- |
- |
| is_private |
bool |
default false |
私有 |
表 cloud_private(私人资料)
| 字段 |
类型 |
键/约束 |
说明 |
| id / passport_id 等 |
- |
PK/归属 |
- |
| data_type |
varchar(50) |
- |
password/card/document… |
| title |
varchar(200) |
- |
标题 |
| description |
varchar(500) |
- |
描述 |
| data |
text |
- |
明文存储的实际数据(未真正加密) |
| is_encrypted |
bool |
default true |
由客户端请求标记,非服务端计算 |
| tags |
varchar(500) |
- |
标签 |
表 cloud_share(分享)
| 字段 |
类型 |
键/约束 |
说明 |
| id / passport_id 等 |
- |
PK/归属 |
- |
| share_type |
varchar(20) |
- |
file/album/note… |
| resource_id |
uint |
- |
资源 ID(建分享时不校验归属) |
| share_token |
varchar(32) |
uniqueIndex |
分享令牌 |
| password |
varchar(100) |
- |
明文存储的分享密码 |
| expires_at |
time |
- |
过期时间 |
| view_count / download_count |
int |
default 0 |
统计 |
| is_public |
bool |
default false |
读取链路未使用该字段 |
表 cloud_space(空间统计)
| 字段 |
类型 |
键/约束 |
说明 |
| id / passport_id 等 |
- |
PK/归属 |
- |
| key_identifier |
varchar(32) |
uniqueIndex(全局) |
默认写入字面量 "default" |
| total_storage / used_storage / max_storage |
int64 |
- |
容量(写入从不校验上限) |
| file_count/album_count/photo_count/note_count/bookmark_count/private_count |
int |
- |
各类统计 |
5. 核心流程
6. 审计发现
6.1 安全
| 级别 |
位置 |
问题 |
| 高 |
internal/logic/private/encrypt_data.go:19-75、decrypt_data.go:17-81、create_private_data.go:54 |
私人资料"加密"未真正落地。README 宣称"加密存储",但 EncryptData/DecryptData 是无状态 AES 工具:密钥由客户端 in.Key 传入(仅填充/截断到 32 字节),函数不读写任何数据库、不绑定任何 CloudPrivate 记录(auth 被 _ 丢弃)。CreatePrivateData 直接把请求里的 Data 原文与客户端自报的 IsEncrypted 落库(data 为明文/客户端原文,非服务端密文)。即"加密"仅是客户端行为 + 一个标记位,服务端未对存储数据做任何加密保护。 |
| 高 |
internal/logic/share/create_share.go:19-87、get_share.go:30 |
分享链路断裂且可构造越权分享。GetShare/ListShares/DeleteShare 均以 passport_id = auth.ID 限定,仅有创建者能看到自己的分享;而底层资源(文件/笔记/相册)读取仍按 passport_id 隔离,收件人即便通过 ValidateSharePassword 也无法取得他人资源(见第 5 节流程图)。此外 CreateShare 仅校验 share_type/resource_id/cloud_identity,不校验 resource_id 是否属于当前用户,可对任意资源 ID 创建分享记录。 |
| 中 |
internal/logic/share/create_share.go:71、get_share.go:54、list_shares.go:57、models/cloud_share.go:18 |
分享密码明文存储并在响应中回传。Password 以 varchar(100) 明文入库,未做哈希;GetShare/ListShares 直接把 Password 回传给调用方。一旦数据库泄露,分享密码即暴露。 |
| 中 |
internal/logic/share/validate_share_password.go:32,43 |
is_public 在读取链路完全未生效。该校验按 identity 查(无 passport 限制),密码判断为 if share.Password != "" && share.Password != in.Password。对于 is_public=false 但 password="" 的分享,任意持有 identity(UUID)者都能校验通过。该标志位仅被存储与回传,未参与任何访问控制。 |
| 中 |
internal/config/config.go:35、impl/impl.go:23 |
配置未校验 Databases。conf.NotNil(Spec.Service, Spec.Cache) 仅校验 Service/Cache,Databases 缺失时 with.Databases(config.Spec.Databases, nil) 可能返回 nil,DBService 为 nil,首个查询即空指针 panic。 |
| 低 |
internal/logic/*/create_*.go、upload_*.go |
cloud_id/cloud_identity 由请求方自报,未与网关 auth 校验一致。因实际隔离靠 passport_id,该字段被当作装饰性元数据;但 ImportBookmarks 又硬编码为 1/"default"(见 6.2),多处来源不一致,容易被误用。 |
6.2 正确性与逻辑缺陷
| 级别 |
位置 |
问题 |
| 高 |
internal/logic/disk/get_dir.go:91、get_dir_tree.go:97 |
根目录 ParentID 空指针解引用 → panic。模型 CloudDiskDir.ParentID 为 *uint(根目录为 nil,create_dir.go:71 仅当 ParentId>0 才赋值)。GetDir 返回体 ParentId: uint64(*dir.ParentID) 与 GetDirTree 第 97 行均无条件解引用,对任意根目录调用即 panic(gRPC 返回 internal 错误)。list_dirs.go:53 已做 nil 判断,说明作者知晓该可空性,但此处遗漏。 |
| 高 |
internal/logic/space/get.go:36-38,97、get_by_key_identifier.go:12、models/cloud_space.go:12 |
空间默认 key_identifier="default" 与全局唯一索引冲突,多用户仅首名可用。CloudSpace.KeyIdentifier 带 uniqueIndex(全局唯一),而 Space.Get 在无记录时新建并写入字面量 "default"。用户 A 首次调用成功建空间;用户 B 查不到自身空间 → 也尝试建 key="default" → 唯一索引冲突 → Create 失败 → Space.Get 永久报错。即除首名用户外,所有用户空间统计接口不可用。 |
| 高 |
全模块写入路径(upload_file.go、copy_file.go、create_*.go 等) |
容量配额从未校验。Space 表虽有 total_storage/max_storage/used_storage,但 Space.Get 只统计不限制;UploadFile/CopyFile 等在写入前没有任何 used_storage >= max_storage 检查。配额是可被无限突破的"展示值"。 |
| 中 |
internal/logic/disk/move_dir.go:47-49 |
目录环检测不全。MoveDir 仅在 dir.ID == parentDir.ID(直接自环)时拒绝,未检测"把目录移入其自身后代"这类深层环。一旦成环,GetDir/GetDirTree 通过 Preload("Subdirectories") 加载自关联子树时会无限递归(预加载不防环),导致请求挂起或栈溢出。 |
| 中 |
internal/logic/note/insert_attachment.go:59 |
返回 Details 为乱码而非数字 ID。Details: string(rune(attachment.ID)) 把整数 ID 当作 Unicode 码点转成单个字符(如 ID=65→"A"),调用方无法据此定位附件。应为 strconv.FormatUint(...)。 |
| 中 |
internal/logic/bookmark/import_bookmarks.go:68-70 |
导入书签硬编码 CloudID:1, CloudIdentity:"default",与 create_bookmark.go:47 使用请求 in.CloudId/in.CloudIdentity 不一致;导入的书签永远归属到固定云空间,与创建接口语义割裂。 |
| 中 |
internal/logic/space/get.go:61-67、get_by_key_identifier.go:52-58 |
used_storage 全量加载文件行到内存再累加:先 Find(&files) 取全部文件记录,再 for 循环加 Size。文件量大时内存与 DB 压力显著,应使用 SELECT COALESCE(SUM(size),0)。 |
| 中 |
internal/logic/space/get.go:46-100、get_by_key_identifier.go:88-107 |
读取接口内含写副作用。Space.Get/GetByKeyIdentifier 在统计后执行 Save(&space),每次读取都会更新 updated_at 与统计字段(且保存失败仅打印不报错),违背只读语义、放大写竞争。 |
| 低 |
internal/logic/note/increment_views.go:31 |
IncrementViews 受 passport_id 限制,仅本人可自增浏览数;访客/他人打开笔记无法计数,浏览统计实际只能反映"本人打开次数"。 |
| 低 |
internal/logic/note/delete_attachment.go:34-39 |
identity 分支为死代码:if/else 两个分支都写 note_attachments.id = ? 且都用 in.Id;且 NoteAttachment 无 identity 列,按 identity 删除本就不可能。 |
6.3 未完成实现
- 私人资料加密存储未落地:仅有客户端可控的 AES 工具与
is_encrypted 标记,服务端未对落库 data 做加解密(同 6.1)。README 宣称"数据加密存储""安全的加密算法"与实际不符。
- 分享向他人交付资源未闭环:无"凭 share_token/identity 取他人资源"的读取路径,收件人无法通过分享取得内容(同 6.1/6.2)。
is_public 形同虚设。
- 无独立进程入口:
cmd/cli/main.go 仅为 "Hello World" 桩,模块须作为 gRPC 库经 pkgs/all/pkgs/ecmall 聚合运行;README 描述的 cmd/main、make run、swagger、docker-compose、tests 等在本仓库均不存在。
- README 与实现不符:README 声称 EXIF 提取、MIME 自动识别、全文搜索、Redis 缓存策略、swagger 文档、健康检查
/health、/metrics 等;实际代码中均无对应实现,且 README 内嵌 SQL DDL(parent_id BIGINT DEFAULT 0、CREATE TABLE-cloud_disk_file 等)与真实 GORM 模型(*uint 可空、表名不同)不一致。
6.4 健壮性与可维护性
| 级别 |
位置 |
问题 |
| 中 |
list_dirs.go:25-27、list_files.go:25-27、list_albums.go:25-27、list_photos.go:25-27、list_notes.go:25-27、search_notes.go:25-27、list_bookmarks.go:25-27、get_private_data*.go:24-29、search_private_data.go:23-28 等 |
分页 page_size 无上限。各列表均以 if in.GetPageSize() < 10 { in.PageSize = 50 } 兜底,但可传入极大值(如 1000000),无上限保护,存在一次拉取全量导致的内存/响应膨胀风险。 |
| 中 |
internal/config/config.go:35 |
conf.NotNil 未覆盖 Databases,配置遗漏时无清晰报错即进入运行期(同 6.1)。 |
| 低 |
全模块 |
无任何单元测试(*_test.go 缺失)。本次发现的空指针 panic、唯一索引冲突、乱码返回等问题若有测试极易暴露。 |
| 低 |
etc/cloud_dev.yaml:14-16 |
MicroService.Anonymous 含 cloud.ping.hello,但本模块不存在该 service/method(无 ping/hello 实现),属失效配置;当前因无其它匿名项,副作用是所有接口强制鉴权(结果安全,但配置语义错误)。 |
正向确认:全模块所有 DB 查询均使用 GORM 参数化 Where("... = ?", ...),未发现 SQL 字符串拼接或 db.Exec/db.Raw(fmt.Sprintf(...))(已 grep 验证),无 SQL 注入风险。所有写操作均按 passport_id = auth.ID 隔离,未发现跨用户越权读取他人目录/相册/笔记/书签/私人资料。
7. 风险汇总
| 编号 |
级别 |
问题 |
影响面 |
| C1 |
高 |
私人资料加密未落地(明文存储、密钥客户端传入、加解密不落库) |
隐私数据保密性 |
| C2 |
高 |
分享链路断裂 + CreateShare 不校验资源归属,收件人无法取得资源、可对任意资源建分享 |
功能不可用 / 潜在越权构造 |
| C3 |
高 |
根目录 ParentID 空指针解引用 → GetDir/GetDirTree panic |
接口可用性(500) |
| C4 |
高 |
key_identifier 全局唯一索引 + 默认 "default" → 仅首名用户空间可用 |
多用户空间功能 |
| C5 |
高 |
容量配额仅统计不限制,写入无上限校验 |
资源滥用 / 存储失控 |
| C6 |
中 |
分享密码明文存储并回传 |
凭证泄露 |
| C7 |
中 |
is_public 读取链路未生效 |
访问控制语义错误 |
| C8 |
中 |
配置未校验 Databases,DB 可能为 nil → 运行期 panic |
启动后稳定性 |
| C9 |
中 |
MoveDir 目录环检测不全 → 预加载递归死循环 |
接口可用性 / 资源耗尽 |
| C10 |
中 |
insert_attachment 返回乱码 ID |
调用方无法定位资源 |
| C11 |
中 |
ImportBookmarks 硬编码 cloud_id=1/"default" |
数据归属不一致 |
| C12 |
中 |
Space 统计全量加载文件到内存 + 读取接口内含写副作用 |
性能 / 语义 |
| C13 |
中 |
所有列表 page_size 无上限 |
大响应 / 资源膨胀 |
| C14 |
低 |
cloud_id/cloud_identity 自报且多来源不一致 |
数据一致性 |
| C15 |
低 |
IncrementViews 仅本人可自增 |
统计失真 |
| C16 |
低 |
delete_attachment 死代码分支 |
可维护性 |
| C17 |
低 |
无单测 |
可维护性 |
| C18 |
低 |
MicroService.Anonymous 失效配置项 |
配置正确性 |
8. 修复建议(务实项)
- 私人资料加密(C1):若需服务端加密,应在
CreatePrivateData/UpdatePrivateData 落库前用服务端持有的密钥对 Data 字段做 AES-GCM 加密、置 IsEncrypted=true 并忽略客户端传入的 IsEncrypted;EncryptData/DecryptData 应改为对指定 CloudPrivate 记录做加/解密并回写,而非无状态工具(或明确改文档说明其为客户端辅助工具,不在服务端落密)。
- 空间唯一索引冲突(C4):将
KeyIdentifier 的唯一索引改为 (passport_id, key_identifier) 复合唯一,或在 Get 创建默认空间时使用 utils.UUID() 而非字面量 "default",避免全局冲突。
- 空指针 panic(C3):
get_dir.go:91、get_dir_tree.go:97 改为与 list_dirs.go:53 一致的 nil 判断(if dir.ParentID != nil { parentId = uint64(*dir.ParentID) },否则置 0)。
- 容量配额(C5):在
UploadFile/CopyFile 写入前,按 passport_id 取 CloudSpace,校验 used_storage + size <= max_storage,超限返回明确错误。
- 分享闭环(C2):为收件人提供"凭
share_token/identity + 密码取资源"的读取路径(资源读取接口支持分享者 passport 或经分享校验放行),并在 CreateShare 校验 resource_id 归属当前用户。
- 分享密码(C6/C7):
Password 入库前做哈希(如 bcrypt),ValidateSharePassword 用哈希比对;并在该校验中真正使用 is_public(非公开且要求密码时才校验密码,公开分享直接放行)。响应中不再回传 Password。
- 目录环(C9):
MoveDir 在移动前递归检查目标目录是否为当前目录的后代,是则拒绝;或限制 GetDirTree/GetDir 的预加载深度。
- 小缺陷:
insert_attachment.go:59 改用 strconv.FormatUint(attachment.ID,10);import_bookmarks.go 与 create_* 统一 cloud_id/cloud_identity 来源;列表 page_size 增加上限(如 min(in.PageSize, 200));Space.Get 统计改为 SELECT COALESCE(SUM(size),0) 且不带写副作用(或拆出独立统计刷新接口);config.NotNil 补 Databases;删除 delete_attachment.go 死代码分支;清理失效的 cloud.ping.hello 匿名配置。
- 补测试:针对根目录 GetDir、多用户 Space.Get、大 page_size、目录环、附件 ID 返回等补最小正例/边界用例。
本报告只列与现有实现直接相关的修复项,不引入统一框架、抽象层、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 |
高 |
私人资料加密未落地(明文存储、密钥由客户端传入) |
已修复:新增 internal/logic/private/crypto.go,改用服务端密钥对内容做 AES-GCM 加解密后落库、读取时解密;客户端传入的 key 不再充当服务端密钥。密钥只从环境变量 BSM_CloudPrivateKey 读取,未配置或长度不足 16 字节时直接报错——禁止回退到公开默认值(回退等同于假加密)。历史明文记录按原值返回,避免旧数据不可读 |
| C2 |
高 |
分享链路断裂 + CreateShare 不校验资源归属 |
已修复:建分享前校验目标资源属于 auth.Identity;读取路径打通,收件人可真正取到资源 |
| C3 |
高 |
根目录 ParentID 空指针解引用导致 panic |
已修复:GetDir/GetDirTree 先判空再解引用 |
| C4 |
高 |
key_identifier 全局唯一 + 写入字面量 "default" |
已修复:取值改为按用户身份唯一,uniqueIndex 改回普通 index,多用户空间可用 |
| C5 |
高 |
容量配额只统计不限制 |
已修复:新增 internal/logic/disk/quota.go,写入路径校验「已用 + 本次 ≤ 配额」,超限返回明确错误 |
未纳入本轮范围
报告中「中」「低」级别的项(分页上限、死代码、README 与实现不符、单测缺失、可维护性等)本轮未处理;如需继续,按各报告第 8 节「修复建议」的顺序推进即可。
部署前置(本次新增)
| 环境变量 |
用途 |
未配置后果 |
BSM_CloudPrivateKey |
私人资料内容 AES-GCM 加解密密钥(≥16 字节,建议 32 字节) |
私人资料写入/读取接口返回内部错误,不再以公开默认密钥「假加密」 |
本轮整改未修改任何 proto/*.proto 与 pb/*.go,因此少数需要新增接口字段才能完整实现的项目(已在处理结果中标注)做了安全降级。