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

398 lines
33 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# cloud个人云空间与资料管理代码审计报告
| 项 | 内容 |
| --- | --- |
| 审计对象 | `module/base/cloud` |
| 服务域 | 基础与平台服务(个人云空间:网盘 / 相册 / 笔记 / 收藏 / 私人资料 / 分享 / 空间统计) |
| 审计日期 | 2026-09-22 |
| 代码规模 | Go 文件约 80 个全部手写7 个 server 注册、59 个 logic 实现、11 个 models、config/impl/cmd 各 1proto 9 个、etc yaml 3 个、README 1 个;生成代码 `pb/*.go` 仅看接口签名 |
| 入口 | 无独立 `cmd/main`(仅 `cmd/cli/main.go` 为 "Hello World" 桩);作为 gRPC 服务经聚合入口 `pkgs/all``pkgs/ecmall` 接入 |
| 对外协议 | gRPC7 个 service+ HTTP Gatewaygrpc-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)。
### Disk15
| 方法 | 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` |
### Album12
| 方法 | 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` |
### Bookmark6
| 方法 | 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` |
### Note10
| 方法 | 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` |
### Private9
| 方法 | 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` |
### Share5
| 方法 | 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` |
### Space2
| 方法 | 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=""` 的分享,任意持有 identityUUID者都能校验通过。该标志位仅被存储与回传未参与任何访问控制。 |
| 中 | `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 行均无条件解引用,**对任意根目录调用即 panicgRPC 返回 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. **空指针 panicC3**`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 改造。
## 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`,因此少数需要新增接口字段才能完整实现的项目(已在处理结果中标注)做了安全降级。