374 lines
31 KiB
Markdown
374 lines
31 KiB
Markdown
# 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. 核心流程
|
||
|
||
```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 改造。
|