Phase 1 安全热修复: - P0-1: /uploads 文件服务添加 JWT 认证中间件(支持 header + query param) - P0-2: analytics/batch 路由从 public 移到 protected_routes - P0-3: plugin engine SQL 注入修复(format! → 参数化查询) - P0-new: stats_service compute_avg_field 字段白名单 + FLOAT8 类型转换 Phase 2 数据完整性: - P0-4: 组织删除级联检查(添加部门存在性校验) - P0-5: 部门删除级联检查(添加岗位 + 用户存在性校验) - P0-8: workflow on_tenant_deleted 实现 5 实体批量删除 - P0-7: 并行网关 race condition 修复(consumed → completed 原子转换) Phase 3 P1 后端 Bug: - P1-12: plugin host 表名消毒(使用 sanitize_identifier) - P1-10: workflow deprecated 状态转换(published → deprecated) - P1-11: workflow 更新验证条件(nodes/edges 任一变化即验证) - P0-9: 小程序 .gitignore 添加 .env/.env.*/日志 - P1-19: 小程序加密密钥替换为 64 字符强密钥 Phase 4 消息模块: - P1-5: 通知偏好 GET 路由 + handler - P1-4: 消息模板 update/delete CRUD + version - P2-8: mark_all_read SQL 添加 version + 1 - P2-7: markAsRead 改为乐观更新 + 失败回滚 Phase 5 前端修复: - P2-9: 通知面板点击导航到 /messages - P2-1: 随访任务患者名批量 ID 解析(替代 UUID 显示) - P2-5: AppointmentList 分离 patient_id/doctor_id 分别调用 API - P2-17: PluginMarket installed 字段修正(name → id) - P3-3: 路由标题 fallback 改为模式匹配(支持 :id 动态路径) - P2-15: workflow updateDefinition 添加 version 字段 - P3-9: Kanban 版本使用记录实际 version - P2-21: secure-storage 生产环境无密钥时阻止存储 - P3-11: destroyOnHidden → destroyOnClose - P3-13: PendingTasks 深色模式 Tag 颜色适配 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
17 KiB
17 KiB
HMS 全系统前端审计报告
日期: 2026-04-26 | 审计范围: 全系统(基础模块 + 健康模块 + 插件 + 小程序) 方法: 代码扫描 + 浏览器操作测试 + 后端日志分析 审计人: Claude Code 自动化审计
执行摘要
| 指标 | 值 |
|---|---|
| 审计覆盖模块 | 12 个(auth/config/workflow/message/plugin/health/ai/settings/小程序) |
| 浏览器验证页面 | 18 个 Web 页面 + 通知面板 |
| 代码级审计模块 | 10 个(4 个后台 agent + 6 个手动/浏览器) |
| 总问题数 | 72 个 |
| P0 阻断 | 9 个 |
| P1 严重 | 20 个 |
| P2 中等 | 22 个 |
| P3 轻微 | 15 个 |
| P4 优化 | 6 个 |
风险矩阵
| 风险域 | 状态 | 关键发现 |
|---|---|---|
| 安全 | 🔴 高风险 | 2 个 P0(文件无认证访问 + SQL 注入) |
| 数据完整性 | 🔴 高风险 | 级联删除缺失 × 3,并行网关死锁 |
| 功能完整性 | 🟡 中等 | 多个模块 CRUD 不完整(模板/偏好/组织) |
| 实时性 | 🟡 中等 | 消息 60 秒轮询,无 WebSocket |
| 代码质量 | 🟢 良好 | 动态表 SQL 注入防护好,租户隔离一致 |
P0 阻断问题(8 个)
P0-1: 上传文件无认证访问
- 模块: erp-server 静态文件服务
- 类型: 安全漏洞
- 复现步骤: 直接访问
http://localhost:3000/uploads/{任意文件名} - 预期行为: 需 JWT 认证才能访问医疗文档
- 实际行为: 所有上传文件公开可访问,包括医疗文档
- 影响: 医疗隐私数据泄露,违反 HIPAA 合规
- 相关文件:
crates/erp-server/src/main.rs:545-546 - 建议修复: 在 ServeDir 前添加 JWT 认证中间件,或使用签名 URL
P0-2: analytics/batch 端点公开
- 模块: erp-server API
- 类型: 安全漏洞
- 复现步骤: 无需认证 POST
/api/v1/analytics/batch - 影响: 任意 JSON 事件注入,可伪造分析数据
- 相关文件:
crates/erp-server/src/main.rs路由注册 - 建议修复: 添加 JWT 认证守卫
P0-3: load_plugin_config SQL 注入
- 模块: erp-plugin 引擎
- 类型: 安全漏洞
- 复现步骤: 恶意 WASM 插件 manifest 中 craft
plugin_id包含 SQL 注入 payload - 影响: 通过
format!()拼接 SQL,plugin_id仅用replace('\'', "''")转义,不安全 - 相关文件:
crates/erp-plugin/src/engine.rs:630-637 - 建议修复: 改用参数化查询
$N占位符
P0-4: 组织删除无级联检查
- 模块: erp-auth 组织管理
- 类型: 数据完整性
- 复现步骤: 删除包含子部门/岗位/用户关联的组织
- 影响: 软删除组织后,子部门、岗位、用户关联成为孤儿数据
- 建议修复: 删除前检查并阻止存在子记录的删除操作
P0-5: 部门删除无级联检查
- 模块: erp-auth 部门管理
- 类型: 数据完整性
- 复现步骤: 删除包含子部门或关联用户的部门
- 影响: 同 P0-4,部门树结构断裂
- 建议修复: 递归检查子部门和用户关联
P0-6: 岗位删除无级联检查
- 模块: erp-auth 岗位管理
- 类型: 数据完整性
- 影响: 删除岗位后用户-岗位关联悬空
- 建议修复: 检查用户关联后阻止或级联更新
P0-7: 并行网关 Join 逻辑可能导致死锁
- 模块: erp-workflow 执行引擎
- 类型: 功能缺陷
- 相关文件:
crates/erp-workflow/src/engine/executor.rs:369-425 - 影响: 并行分支汇聚时,token 查询可能匹配到历史迭代的 stale token,导致提前/错误完成或永久等待
- 建议修复: 添加 token lineage/correlation 机制,区分不同 fork 产生的 token
P0-8: workflow on_tenant_deleted 是空操作
- 模块: erp-workflow 租户清理
- 类型: 数据完整性
- 相关文件:
crates/erp-workflow/src/module.rs:149-154 - 影响: 删除租户后,流程定义/实例/任务/Token 残留,跨租户数据泄漏风险
- 建议修复: 实现级联软删除所有租户相关数据
P1 严重问题(18 个)
P1-1: health-data 统计端点 500 错误
- 模块: erp-health 统计
- 类型: 功能缺陷
- 根因: PostgreSQL AVG() 返回 NUMERIC 类型,Rust 代码期望 f64(FLOAT8)
- 相关文件:
crates/erp-health/src/service/stats_service.rs - 浏览器验证: 统计报表页面显示"加载统计数据失败 500"
- 建议修复: 使用
Decimal类型或 SQL 中显式CAST(AVG(...) AS FLOAT8)
P1-2: 行级数据权限未生效
- 模块: erp-auth 权限
- 类型: 功能缺陷
- 影响:
department_ids已填充但未用于数据过滤查询 - 建议修复: 在查询构建器中加入 department_ids 过滤条件
P1-3: 消息无实时推送
- 模块: erp-message
- 类型: 功能缺失
- 影响: 医疗告警最多延迟 60 秒才能到达医护人员,临床不可接受
- 建议修复: 实现 WebSocket 或 SSE 推送
P1-4: 消息模板 CRUD 不完整
- 模块: erp-message
- 类型: 功能缺失
- 影响: 模板只能创建和列表,无法编辑、删除,且
render()方法未接入发送管道 - 相关文件:
crates/erp-message/src/template_service.rs
P1-5: 通知偏好设置无法加载已有配置
- 模块: erp-message + 前端
- 类型: 功能缺陷
- 影响: 后端无 GET
/message-subscriptions端点,前端无法加载用户已保存的偏好;第二次保存因缺少 version 字段必然失败 - 相关文件:
crates/erp-message/src/module.rs,NotificationPreferences.tsx
P1-6: 工作流 ServiceTask 是空操作
- 模块: erp-workflow
- 类型: 功能缺失
- 相关文件:
crates/erp-workflow/src/engine/executor.rs:277 - 影响: 所有 ServiceTask 被自动跳过,
service_type字段无效
P1-7: 工作流未注册任何事件处理器
- 模块: erp-workflow
- 类型: 功能缺失
- 影响:
register_event_handlers为空函数,工作流模块不响应任何外部事件 - 相关文件:
crates/erp-workflow/src/module.rs:137
P1-8: candidate_groups(角色/部门分配)存储但未使用
- 模块: erp-workflow
- 类型: 功能缺失
- 影响: 配置了 candidate_groups 但无 assignee 的 UserTask 对所有用户不可见,任务成为孤儿
- 相关文件:
crates/erp-workflow/src/service/task_service.rs:25-36
P1-9: 工作流超时检查仅记录日志
- 模块: erp-workflow
- 类型: 功能缺失
- 影响: 超时任务无升级/自动完成/通知,永久停留在 pending 状态
- 相关文件:
crates/erp-workflow/src/engine/timeout.rs
P1-10: 工作流 deprecated 状态不可达
- 模块: erp-workflow + 前端
- 类型: 功能缺陷
- 影响: 前端定义了
deprecated状态样式,但后端无转换路径 - 相关文件:
ProcessDefinitions.tsx:19
P1-11: 工作流定义更新跳过校验
- 模块: erp-workflow
- 类型: 功能缺陷
- 影响: 只更新 nodes 而不提供 edges 时,不做图结构验证
- 相关文件:
crates/erp-workflow/src/service/definition_service.rs:174-181
P1-12: 编号序列表名未充分消毒
- 模块: erp-plugin
- 类型: 安全隐患
- 影响:
plugin_id格式化到 DDL/DML 语句时仅replace('-', "_"),不处理引号/分号 - 相关文件:
crates/erp-plugin/src/host.rs:339
P1-13 ~ P1-18: 组织模块 P1 问题
- P1-13: 组织/部门/岗位缺少列表 API 中的树形结构返回
- P1-14: 部门树重组操作(拖拽移动父节点)未实现
- P1-15: 组织/部门名称唯一性校验缺失
- P1-16: 部门详情 GET 端点缺失
- P1-17: 岗位分配/取消分配 API 缺失
- P1-18: 消息群发(角色/部门/全员)fan-out 未实现
P2 中等问题(20 个)
| ID | 模块 | 问题 | 文件 |
|---|---|---|---|
| P2-1 | health | 随访任务患者名显示为 UUID 片段 | FollowUpTaskList.tsx |
| P2-2 | health | 前端测试覆盖率极低(3 个文件) | apps/web/src/ |
| P2-3 | health | 深色模式样式重复 ~19 页内联 isDark | 各健康页面 |
| P2-4 | health | useDarkMode 和 useThemeMode 重叠 | hooks/ |
| P2-5 | health | AppointmentList 冗余 404 请求 | AppointmentList.tsx |
| P2-6 | message | 未读计数不即时刷新(等 60s 轮询) | stores/message.ts |
| P2-7 | message | markAsRead 失败不回滚乐观更新 | stores/message.ts:57-69 |
| P2-8 | message | mark_all_read 不更新 version 字段 | message_service.rs:298-326 |
| P2-9 | message | 通知面板点击不导航到详情 | NotificationPanel.tsx:81-85 |
| P2-10 | message | 轮询间隔 60s 对医疗告警不可接受 | NotificationPanel.tsx:28-31 |
| P2-11 | workflow | N+1 查询问题(实例/任务列表) | instance_service.rs, task_service.rs |
| P2-12 | workflow | 排他网关表达式错误被静默吞掉 | executor.rs:176 |
| P2-13 | workflow | ProcessDesigner 不支持边条件配置 | ProcessDesigner.tsx |
| P2-14 | workflow | 委派 API 要求手动输入 UUID | PendingTasks.tsx:207-210 |
| P2-15 | workflow | 前端 UpdateDefinitionRequest 缺少 version | workflowDefinitions.ts:45-51 |
| P2-16 | plugin | reconcile_references 表名注入模式 | data_service.rs:1204-1206 |
| P2-17 | plugin | PluginMarket installed 判断字段不匹配 | PluginMarket.tsx:68 |
| P2-18 | plugin | 模板渲染未接入发送管道 | template_service.rs:92-99 |
| P2-19 | plugin | 偏好设置 version 字段未发送导致更新失败 | NotificationPreferences.tsx:26-37 |
| P2-20 | plugin | 偏好设置仅暴露 DND,channel_preferences 隐藏 | NotificationPreferences.tsx:60 |
P3 轻微问题(14 个)
| ID | 模块 | 问题 |
|---|---|---|
| P3-1 | health | 已完成任务仍显示操作按钮 |
| P3-2 | health | ArticleEditor 图片上传未实现 (TODO) |
| P3-3 | health | PatientDetail 头部标题显示"页面" |
| P3-4 | health | 4 处 any 类型使用 |
| P3-5 | health | 登录硬编码默认 tenant_id |
| P3-6 | settings | 审计日志操作用户列显示原始 UUID |
| P3-7 | settings | 审计日志资源类型过滤列表硬编码 |
| P3-8 | settings | 系统参数无列表 API,需手动输入 key |
| P3-9 | plugin | Kanban 拖拽 version 硬编码 0 导致锁冲突 |
| P3-10 | plugin | CRUD 排序使用 JSONB 文本提取导致字典序 |
| P3-11 | workflow | ProcessDesigner 用 destroyOnHidden 而非 destroyOnClose |
| P3-12 | workflow | InstanceMonitor 显示 raw node_id 而非名称 |
| P3-13 | workflow | 待办任务状态 Tag 不适配深色模式 |
| P3-14 | message | 偏好设置 DND 启用但未填时间范围时无效 |
P4 优化建议(6 个)
| ID | 模块 | 建议 |
|---|---|---|
| P4-1 | plugin | PluginAdmin purge 按钮状态与后端不一致 |
| P4-2 | plugin | WASM init() 使用 nil UUID |
| P4-3 | plugin | recover_plugins 不按 tenant_id 过滤 |
| P4-4 | settings | LanguageManager 编辑弹窗无可编辑字段 |
| P4-5 | settings | ChangePassword 最小长度仅前端校验 |
| P4-6 | settings | Settings API delete/update URL 编码不一致 |
模块审计摘要
基础模块
| 模块 | 页面数 | 浏览器验证 | 代码审计 | 关键发现 |
|---|---|---|---|---|
| 用户/权限 (B1) | 2 | ✅ | ✅ | email 验证宽松 |
| 组织架构 (B2) | 1 | ❌ | ✅ | 3×P0 级联删除缺失 |
| 工作流 (B3) | 6 | ✅ 3 页 | ✅ | 3×P0 + 6×P1 功能大量缺失 |
| 消息 (B4) | 3+面板 | ✅ 面板 | ✅ | 5×P1 无实时推送 |
健康模块
| 模块 | 页面数 | 浏览器验证 | 关键发现 |
|---|---|---|---|
| 患者管理 (B5) | 2 | ✅ | 标题"页面"bug |
| 医生/排班/预约 (B6) | 3 | ✅ | 冗余 404 请求 |
| 随访/咨询 (B7) | 4 | ✅ 部分 | 患者 UUID 片段 |
| 积分/文章/AI (B8) | 9 | ✅ 部分 | 统计报表 500 |
系统/插件
| 模块 | 页面数 | 浏览器验证 | 代码审计 | 关键发现 |
|---|---|---|---|---|
| 插件 (B9) | 8 | ❌ | ✅ | P1 SQL 注入 |
| 统计/仪表盘 (B9) | 2 | ✅ | ✅ | 500 错误 |
| 系统设置 (B10) | 8 标签 | ✅ 3 标签 | ✅ | 审计日志 UUID |
小程序(B11-B14)
| 模块 | 审计方式 | 关键发现 |
|---|---|---|
| 登录/首页 (B11) | 代码审计 | P0 .env 泄露风险 |
| 健康管理 (B12) | 代码审计 | P2 错误处理缺失 |
| 医患交互 (B13) | 代码审计 | P1 咨询消息轮询无错误恢复 |
| 内容/商城 (B14) | 代码审计 | P2 空状态处理缺失 |
小程序专项发现
P0-9: .env 未加入 .gitignore
- 模块: miniprogram 配置
- 类型: 安全漏洞
- 相关文件:
apps/miniprogram/.gitignore(仅含 node_modules/ 和 dist/) - 影响:
.env文件含TARO_APP_ENCRYPTION_KEY=hms_miniprogram_encryption_key_2026,可能意外提交到 git - 当前状态: 未被 git 追踪(git ls-files 为空),但风险存在
- 建议修复: 在
.gitignore中添加.env和.env.*
P1-19: 小程序加密密钥为弱密钥
- 模块: miniprogram secure-storage
- 类型: 安全隐患
- 相关文件:
apps/miniprogram/.env:2,apps/miniprogram/src/utils/secure-storage.ts - 影响:
hms_miniprogram_encryption_key_2026为可预测字符串,AES 加密形同虚设 - 建议修复: 使用
python -c "import secrets; print(secrets.token_hex(32))"生成强密钥
P1-20: project.config.json urlCheck: false
- 模块: miniprogram 配置
- 类型: 安全隐患
- 相关文件:
apps/miniprogram/project.config.json:6 - 影响: 生产环境允许请求任意域名,应限制为后端 API 域名
- 建议修复: 生产版本设为
true并配置合法域名白名单
P2-21: secure-storage 未设密钥时明文存储
- 模块: miniprogram secure-storage
- 类型: 安全隐患
- 相关文件:
apps/miniprogram/src/utils/secure-storage.ts:12 - 影响:
ENCRYPTION_KEY为空时encrypt()直接返回明文,token 和敏感数据不加密存储 - 建议修复: 强制要求密钥,未设置时阻止存储敏感数据
P2-22: 小程序各页面缺少统一错误边界
- 模块: miniprogram 全局
- 类型: 功能缺陷
- 影响: API 请求失败时页面无友好错误提示,可能白屏
- 建议修复: 添加全局错误边界组件和网络错误拦截器
P3-15: 小程序部分页面缺少空状态处理
- 模块: miniprogram 健康管理
- 类型: UX 问题
- 影响: 健康数据为空时无引导提示
浏览器验证发现汇总
以下问题通过实际浏览器操作验证:
| 页面 | 操作 | 结果 | 问题 |
|---|---|---|---|
| Users | 创建用户(无效邮箱) | 成功 | email 验证宽松 |
| Users | 删除用户 | 成功 | — |
| Users | 分配角色 | 成功 | — |
| Patient List | 搜索 | 成功 | — |
| Patient Detail | 切换标签 | 成功 | 标题显示"页面" |
| Statistics | 加载 | 失败 500 | SQL 类型不匹配 |
| Settings-字典 | 新建/添加项/删除 | 全部成功 | — |
| Settings-密码 | 错误旧密码 | 正确提示 | — |
| Settings-审计日志 | 查看 | 成功 | UUID 而非用户名 |
| Settings-菜单 | 查看 | 成功 | — |
| Workflow-定义 | 列表 | 成功 | — |
| Workflow-设计器 | 编辑草稿 | 成功 | 缺节点属性编辑器 |
| Workflow-待办 | 查看 | 成功 | — |
| 通知面板 | 打开 | 成功 | 60s 轮询延迟 |
优先修复建议
第一优先级(P0,必须立即修复)
- 上传文件认证(P0-1)— 医疗隐私合规要求
- SQL 注入修复(P0-3)— 安全风险,改用参数化查询
- analytics/batch 认证(P0-2)— 防止数据伪造
- 级联删除检查(P0-4/5/6)— 防止数据完整性破坏
- 并行网关修复(P0-7)— 核心工作流引擎可靠性
- 租户清理实现(P0-8)— 多租户数据隔离
第二优先级(P1,2 周内修复)
- 统计报表 SQL 类型修复(P1-1)— 面向用户的功能
- 行级数据权限实现(P1-2)— 安全要求
- 消息实时推送(P1-3)— 医疗告警时效性
- 工作流功能补全(P1-6~11)— 模块可用性
- 消息模板/偏好完善(P1-4/5)— 功能闭环
第三优先级(P2,1 月内修复)
- 前端测试覆盖率提升(P2-2)
- 工作流 N+1 查询优化(P2-11)
- 各模块 UX 修复(UUID 显示、标题、按钮状态等)
附录:审计方法
- 代码扫描: Explore agent 深度遍历源码,逐模块检查功能完整性
- 浏览器测试: Chrome DevTools MCP 实际操作 18 个页面,验证 CRUD 链路
- 后端日志分析: 解析 Axum tracing 日志定位 500 错误根因
- 网络面板: 监控 API 请求/响应,发现冗余调用和错误响应