unis_crm/系统审计分析报告.md

21 KiB
Raw Blame History

UNIS CRM 系统全量审计分析报告

  • 审计日期2026-09-01
  • 审计范围后端Spring Boot + MyBatis、前端frontend / frontend1 两个 React 工程、SQL 脚本、Docker/Nginx 部署配置
  • 审计方式:静态代码审查(未改动任何代码)
  • 结论概览:商机/渠道/CRM 拓展互移、商机 tab 阶段筛选导出等核心业务逻辑正确;但存在 2 个越权类安全漏洞、1 个前端业务阻断缺陷、1 个全新部署致命缺口 及若干配置/一致性问题,建议按优先级修复。

一、严重问题(建议优先修复)

1. 打卡照片 / 日报附件下载接口存在越权访问IDOR

2. 生产配置硬编码大量敏感凭据,且 jwt-secret 使用默认值

  • 位置:application.yml#L4-L10、L20-L29、L53、L78、L88application-prod.yml#L51
  • 问题:
    • MinIOadmin/Admin@123456、PostgreSQLunis@123、Rediszghz@123)、企微 secret、OMS api-key 全部明文写入仓库。
    • 两个 profile 的 jwt-secret 均为默认值 change-me-please-change-me-32bytes,且 docker-compose 未通过环境变量覆盖(docker-compose.yml#L14-L16 仅传 TZSPRING_PROFILES_ACTIVE)。一旦使用默认值,攻击者可伪造任意用户/租户的 access token。
  • 建议:改为注入环境变量/密钥管理,强制覆盖 jwt-secret

3. 日报提交失败后前端永久锁死,需刷新页面才能恢复

  • 位置:Work.tsx#L2175-L2226
  • 问题:reportSubmitInFlightRef.current = truesetReportSubmitLocked(true)L2205-L2206 设置,但只在成功路径L2214-L2215复位。一旦 saveWorkDailyReport 抛错网络异常、后端校验失败、401 等),两个标记永久保持 true,提交按钮被守卫 L2176 永久禁用,提示「提交未确认,请刷新后重试」。当日已填写的整份日报(行项/附件/明日计划)必须刷新页面才能再次提交,丢失风险高。
  • 建议:将两个标记的复位移入 catchfinally(与 setSubmittingReport(false) 一起)。

4. 全新安装脚本 init_full_pg17.sql 缺少 4 张运行时依赖表

  • 位置:init_full_pg17.sql(自声明为全新环境权威初始化入口)
  • 问题:该脚本没有创建以下后端实际依赖的表:
  • 这 4 张表只存在于增量脚本 20260827.sql#L45、L77、L92、L142;已核实 8 个启动期 SchemaInitializer 均未补建common/ 下只有 dashboard/日历/日报提醒/语音/数据权限等表)。
  • 影响:全新库只跑 init_full_pg17.sqlCRM 拓展模块、商机 OMS 推送/回传、渠道覆盖地市会在运行时抛 relation does not exist。生产库(已执行 20260827.sql不受影响但属部署链路硬缺口。
  • 建议:在 init_full_pg17.sql 中补齐这 4 张表(可直接并入 20260827.sql 的定义)。

5. OMS 集成回写的阶段/状态校验与新阶段码不一致(残留死代码,不阻断实际同步)

  • 位置:OpportunityServiceImpl.java#L920-L925OpportunityServiceImpl.java#L944-L974
  • 已确认用户核实OMS 反写只根据 archived 字段触发「已签约」,不下发 status=won/lost。因此 archived=true → 强制 stage=S5 路径(L922-L925)实际可正常同步「已签单」,不构成阻断
  • 残留问题(低危):
    • resolveIntegrationStatus 仅识别字面量 "won"/"lost" 阶段,但阶段经 mapStageToCrm 映射后为 S5/L,这两个分支是死代码WON_STAGE_CODES = Set.of("won","S6")L65)定义后从未使用,且 S6 实为 OMS 码,佐证旧逻辑未随阶段码迁移同步更新。
    • 若 OMS 未来开始下发 status=won/lost,仍会走到 switchcase "won","lost" 抛错;若 OMS 下发 stage(如 L)但不带 status商机 status 会落为 active(语义不符,但 crm_opportunity.status 允许 active,不报错)。
  • 建议:后续可在 resolveIntegrationStatus 中按映射后新码S5→won、L→lost推导 status并清理死代码 WON_STAGE_CODES;非阻塞,可留作低优先维护项。

二、业务逻辑问题

6. 打卡可重复提交(当日无去重)

7. 日报导出按「业务类型」筛选在 5000 条截断之后执行

  • 位置:WorkServiceImpl.java#L316-L321
  • 问题:exportDailyReports 先合并去重并 limit(EXPORT_LIMIT=5000),再用内存 hasReportLineOfType 过滤;而打卡导出的 bizType 是在 SQL 内过滤(WorkMapper.xml#L35-L37)。日期区间大、其它类型行数多时,目标类型日报会被 5000 上限截断,导出结果偏少/为空。

8. 打卡日期边界使用 DB current_date,与日报 Asia/Shanghai 口径不一致

9. Dashboard「当前阶段」硬编码旧枚举与商机/拓展新阶段码不一致

10. 跟进记录新增接口返回的 ID 是「影响行数」而非真实主键

11. 商机导出采用「全量拉取 + 前端过滤」

  • 位置:Opportunities.tsx#L2255
  • 问题:导出调用 getOpportunityOverview("", undefined, false, null, ...)limit=null 一次性拉取租户内全部商机再在客户端过滤。商机规模大时单次请求体量大、耗时长,存在超时/内存风险。建议改为服务端筛选或分批拉取。

12. 语音识别配置页缺少「未选租户」防护

13. 前端 request()fetchWithAuth() 对 401 处理不一致

  • 位置:auth.ts#L1259-L1271 vs auth.ts#L1298-L1311
  • 问题:request() 遇 401 直接 handleUnauthorizedResponse()(清登录态跳登录页),不做「刷新+重试」;fetchWithAuth() 会先刷新再重试。token 被提前吊销或服务端判定失效但本地未过期时,用户会在操作中被直接登出并丢失已输入内容。

14. 附件上传走 XHR 但未做 token 主动刷新

  • 位置:auth.ts#L1668-L1723
  • 问题:uploadWorkReportAttachment 直接读 localStorage.accessToken 发 XHR不经过 ensureFreshAccessToken。长时间编辑后 token 到期,上传 401 → 直接登出,正在填写的日报/打卡表单数据丢失。建议上传前先刷新 token。

三、潜在风险 / 代码规范

# 问题 位置
15 生产环境 MyBatis log-impl: StdOutImpl 打印全部 SQL 与绑定参数(含敏感数据) application.yml#L36
16 语音转写整文件读入内存、无独立大小限制;打卡照片无大小限制(依赖全局 500MB可致 OOM/存储膨胀 WorkServiceImpl.java#L516-L573WorkServiceImpl.java#L448-L465
17 定时任务共享默认单线程调度器,ReportReminderScheduler(每分钟)与 BusinessCalendarAutoSync(每日 3:15可能相互阻塞且无分布式锁 ReportReminderScheduler.javaBusinessCalendarAutoSync.java
18 数据权限完全依赖 unisbase 插件在 SQL 上动态注入SQL 本身无兜底条件;插件缺失/非 Web 上下文可能返回全库数据 WorkMapper.java#L32-L72
19 前端权限判断 fail-open权限码列表为空时全部放行if (!codes.length) return true),权限拉取失败时 UI 虚假放行 auth.ts#L1469-L1483
20 20260828update.sqlUPDATE ... FROM crm_oms_dict_mapping 依赖 oms_value 唯一,但表唯一约束为 (dict_type, crm_value, oms_value),未约束 (dict_type, oms_value) 唯一,后续新增映射可能产生非确定性更新 20260828update.sql#L106-L11220260827.sql#L104
21 生产 profile 企微默认 enabled:false,与 SSO 期望redirect-uri 指向 crm.unissense.top不一致/api/wecom/sso/**/api/opportunities/integration/** 为 permit-all application-prod.yml#L72-L82
22 frontend1 http.ts 鉴权白名单用 url.includes(path) 子串匹配,路径匹配过度放宽 http.ts#L14、L68-L70
23 生产 MinIO endpointhttps://miniodown.nex.unisspace.comuse_ssl: false,可能连接异常 application-prod.yml#L5-L10
24 后端 Dockerfile 复制 target/unis-crm-backend-1.0.0-SNAPSHOT.jar,但仓库内 jar 位于 backend/ 根目录(非 target/),依赖 CI 先执行 mvn package,否则 docker compose build 会失败 backend/Dockerfile#L7
25 打卡/日报元数据使用字符串内嵌标记([[CHECKIN_PHOTOS]][[WORK_REPORT_LINES]] 等)拼入 remark 字段,解析依赖精确格式,健壮性差 WorkServiceImpl.java#L91-L102
26 ActionDialog 确认回调 void onConfirm() 未捕获 rejection ActionDialog.tsx#L150
27 腾讯地图生产 Key 硬编码进前端源码作兜底 tencentMap.ts#L1
28 移动端多选下拉点击遮罩关闭不「回滚」已勾选项,与常规确定/取消交互不一致,易误改覆盖省市 AdaptiveSelect.tsx#L204-L216
29 浏览器定位最坏路径约 33s弱信号下用户易误以为卡死 tencentMap.ts#L145-L176
30 Dashboard 权限/消息加载静默吞异常,问题被掩盖 DashboardServiceImpl.java#L171-L207

四、已核对且判定为正常的重点项

  1. 商机 tab 阶段筛选/导出约束(与项目硬约束一致)
  2. 渠道拓展/CRM 拓展互移流程:属主校验、字段映射(覆盖省市/办公地址/联系人)、跟进/签到/日报消息的 biz_type 迁移、旧记录清理均正确(ExpansionServiceImpl.java#L398-L495),与前端弹窗字段、校验一致。
  3. 覆盖地市存储模型:后端读写 crm_channel_expansion_coverage 从表(ExpansionMapper.xml#L106-L119insertChannelExpansion 不引用废弃的 coverage 列,与 20260827.sql 的设计一致 ✓
  4. 商机集成 archived→S5 强制:已实现并通过单测(OpportunityServiceImplTest.java#L211-L228);且经确认 OMS 反写仅用 archived 字段触发「已签约」(不下发 status=won/lost),实际同步正常 ✓
  5. SQL 增量脚本幂等性create table if not existsdrop constraint if existson conflict do nothing/update 均正确20260828update.sql 的换算用单条 UPDATE 避免连环转换 ✓
  6. 前后端字段一致性MoveCrmToChannelRequestcoverageProvince/coverageCity/coverageItemsCreateOpportunityRequest 与前端序列化一致confidencePct 正则与前端兜底一致 ✓
  7. Nginx 延迟解析:两个前端 nginx 均使用 resolver + set $backend_host 延迟解析域名,符合约束 ✓(default.conf.template#L8、L16-L19
  8. 前端 Dockerfile/.dockerignore.dockerignore 已排除 node_modules、.git、系统目录多阶段构建合理 ✓
  9. 日报保存逻辑:「当日重复提交→更新」「北京时间 10 点前归属前一天」「提醒在事务提交后触发」均正确(WorkServiceImpl.java#L357-L415

五、修复优先级建议

优先级 问题编号 理由
P0 #1、#2 越权与凭据泄露,安全风险最高
P0 #4 全新部署直接功能瘫痪
P1 #3 前端业务阻断,日报当天无法提交
P2 #6、#7、#8、#9、#11、#13、#14 数据正确性与一致性
P3 #5 及其余 #5 已确认不阻断实际同步OMS 仅用 archived 反写),降为低危维护项

说明:本报告仅做静态审计,未运行代码;部分结论(如 #5 是否实际触发、#11 的量级、#8 的时区)建议结合运行时数据/日志复核后再修复。