31 KiB
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. 核心流程
flowchart TD
A["客户端请求 GetFile(id)"] --> B["ParseMetaCtx 解析 passport"]
B --> C{"有鉴权?"}
C -- 否 --> E["返回错误"]
C -- 是 --> D["JOIN cloud_disk_dirs WHERE passport_id=auth.ID AND file.id=?"]
D --> F{"命中记录?"}
F -- 否 --> G["返回 ErrInvalidArgument"]
F -- 是 --> H["返回文件详情"]
flowchart TD
A["分享者 CreateShare(resource_id)"] --> B["仅存分享记录, 不校验 resource_id 归属"]
C["收件人 ValidateSharePassword(identity,password)"] --> D["WHERE identity=? (无 passport 限制, 不校验 is_public)"]
D --> E{"密码/过期校验"}
E -- 失败 --> F["返回错误"]
E -- 通过 --> G["ViewCount++, 返回 OK"]
G --> H["收件人尝试 GetFile/GetNote(resource_id)"]
H --> I["WHERE passport_id=收件人(非分享者)"]
I --> J["无权限, 返回 ErrInvalidArgument"]
J --> K["资源实际无法被收件人获取"]
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 改造。