# 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./`,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./`(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. 核心流程 ```mermaid 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["返回文件详情"] ``` ```mermaid 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. 修复建议(务实项) 1. **私人资料加密(C1)**:若需服务端加密,应在 `CreatePrivateData`/`UpdatePrivateData` 落库前用服务端持有的密钥对 `Data` 字段做 AES-GCM 加密、置 `IsEncrypted=true` 并**忽略客户端传入的 `IsEncrypted`**;`EncryptData`/`DecryptData` 应改为对指定 `CloudPrivate` 记录做加/解密并回写,而非无状态工具(或明确改文档说明其为客户端辅助工具,不在服务端落密)。 2. **空间唯一索引冲突(C4)**:将 `KeyIdentifier` 的唯一索引改为 `(passport_id, key_identifier)` 复合唯一,或在 `Get` 创建默认空间时使用 `utils.UUID()` 而非字面量 `"default"`,避免全局冲突。 3. **空指针 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)。 4. **容量配额(C5)**:在 `UploadFile`/`CopyFile` 写入前,按 `passport_id` 取 `CloudSpace`,校验 `used_storage + size <= max_storage`,超限返回明确错误。 5. **分享闭环(C2)**:为收件人提供"凭 `share_token/identity` + 密码取资源"的读取路径(资源读取接口支持分享者 passport 或经分享校验放行),并在 `CreateShare` 校验 `resource_id` 归属当前用户。 6. **分享密码(C6/C7)**:`Password` 入库前做哈希(如 bcrypt),`ValidateSharePassword` 用哈希比对;并在该校验中真正使用 `is_public`(非公开且要求密码时才校验密码,公开分享直接放行)。响应中不再回传 `Password`。 7. **目录环(C9)**:`MoveDir` 在移动前递归检查目标目录是否为当前目录的后代,是则拒绝;或限制 `GetDirTree`/`GetDir` 的预加载深度。 8. **小缺陷**:`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` 匿名配置。 9. **补测试**:针对根目录 GetDir、多用户 Space.Get、大 page_size、目录环、附件 ID 返回等补最小正例/边界用例。 > 本报告只列与现有实现直接相关的修复项,不引入统一框架、抽象层、DTO/VO 或 DDD/CQRS 改造。