16 KiB
CMPP 平台代码质量审查报告
- 报告日期:2026-08-28
- 审查类型:当前工作区只读基线审计
- 审查仓库基线:
171c7d38e8f17de0dd83570603da316d47c0d015;对应业务代码基线为其父提交a70e9e2c078a6109dd87cf8e45aeff956faf24e8 - 分支状态:
main,相对origin/main超前 12 个提交 - 审查范围:React/Vite 前端、NestJS API、Prisma/PostgreSQL、Redis/BullMQ 接口、Go Gateway、自动化测试、构建与工程配置
- 明确未执行:提交、推送、部署、数据库写入、短信发送、远程环境变更、生产/预生产利用验证
1. 执行摘要
本次综合判定为:有条件不通过,当前版本不建议继续发布。
代码已经具备较好的业务复杂度承载能力:API 45 个测试套件共 532 项全部通过,Gateway 全包测试与 go vet 通过,前后端正式构建、Prisma Schema、Gateway 队列契约和已有安全部署校验均通过;PostgreSQL 连接池、事务级 advisory lock、FOR UPDATE SKIP LOCKED 和核心业务复合索引也已实际存在。
但审查发现 2 个 P0 发布阻断问题:
- 客户端租户范围来自浏览器可修改的
x-tenant-id请求头,后端多个客户端接口直接信任该值,部分写接口还直接信任请求体中的tenantId,存在跨租户读取、创建、修改或删除业务数据的风险。 - 用户密码使用无盐单次 SHA-256 保存和比对。数据库一旦泄漏,相同密码可直接关联,并可使用 GPU/字典进行高速离线破解,不符合生产账号系统的密码存储要求。
因此,本报告不把“532 项测试通过”解释为生产可发布,也不把现有 mock 单测解释为真实 PostgreSQL、Redis、MinIO、Gateway 和 CMPP 全链验收完成。
2. 质量评级
| 维度 | 评级 | 结论 |
|---|---|---|
| 业务正确性与并发设计 | B | 关键发送链测试较多,存在 advisory lock、幂等和队列 claim 机制;未做本轮真实全链复验。 |
| 安全性 | D | 存在租户越权和弱密码哈希两个 P0。 |
| 自动化测试 | C | API/Gateway 测试数量较好;API 语句覆盖率 59.61%、分支覆盖率 50.99%,前端测试文件为 0,且没有覆盖率门槛。 |
| 前端性能 | C- | 主 JavaScript 包 2.12 MB,gzip 626 KB,所有页面同步进入主包。 |
| 可维护性 | C | 存在超大 Service/Page、无 lint/format 门禁、生产类型仍依赖 src/mock。 |
| 数据库工程 | B | Schema 有效,核心复合索引、连接池和锁策略较完整;未对真实数据执行 EXPLAIN (ANALYZE, BUFFERS)。 |
| 依赖与仓库卫生 | C- | 依赖审计仍有 2 个 high advisory;构建缓存被跟踪,临时文件和产物缺少统一忽略策略。 |
综合参考分:58/100。该分数用于排序治理工作,不等价于功能验收通过率。
3. 发布阻断问题
CQ-SEC-001 / P0:客户端租户身份可由请求方伪造
证据链
src/api/session.ts:175-177从浏览器localStorage中的展示会话读取tenantId。src/api/core/httpClient.ts:90-92、117-119将该值写入x-tenant-id。api/src/common/tenant-id.decorator.ts:3-6直接返回客户端请求头中的x-tenant-id,没有绑定已认证用户。api/src/auth/session-validation.middleware.ts:25-55验证会话用户、状态和 portal,但没有把用户所属租户写成服务端可信租户上下文,也没有校验请求头租户等于用户租户。api/src/certification/certification.controller.ts:13-19的客户端认证查询信任请求头,提交接口直接接受请求体。api/src/certification/certification.service.ts:49-82的提交逻辑直接使用data.tenantId创建认证记录、更新目标企业并写操作日志。api/src/sms-config/client-sms-config.controller.ts:26-163大量客户端查询和写接口依赖同一@TenantId();其中应用密钥重置、应用状态变更、材料创建等接口甚至没有传入租户范围。
影响
已登录企业管理员可以修改浏览器存储、请求头、请求体或资源 ID,尝试访问其他企业的认证、应用、签名、模板、账务、日志等数据。文件服务已有“从当前会话用户反查租户”的正确实现,但该保护没有成为客户端 API 的统一规则。
最小修复要求
- 在服务端认证中间件中根据
sessionUserId获取并写入可信request.tenantId;客户端控制器只能读取该字段。 - 客户端路由禁止把
x-tenant-id或请求体tenantId作为授权依据;即使保留请求头,也只能用于一致性检查,不得决定数据范围。 - 所有按资源 ID 的客户端读写在数据库查询中同时带上可信
tenantId。 - 增加 tenant-a/tenant-b 的真实 API 越权回归,覆盖查询、创建、更新、删除、密钥重置、上传下载和导出。
CQ-SEC-002 / P0:用户密码采用无盐 SHA-256
证据链
api/src/users/users.service.ts:444-445:createHash('sha256').update(password).digest('hex')。api/src/users/users.service.ts:141、213、267使用该函数创建或修改密码。api/src/auth/auth.service.ts:62使用字符串相等比较验证密码。
影响
密码哈希没有用户级 salt,也没有故意增加计算和内存成本。数据库泄漏后,弱密码可被高速离线破解;相同密码生成相同哈希,还会泄露账号之间的密码复用关系。
最小修复要求
- 改用 Argon2id;如运行环境暂不支持,可使用带独立 salt 和合理成本参数的 scrypt/bcrypt。
- 新哈希保存算法版本与参数;验证旧 SHA-256 成功后立即透明升级,避免一次性强制所有账号重置。
- 使用库提供的恒定时间验证接口,不直接比较哈希字符串。
- 增加旧哈希迁移、错误密码、参数升级、密码修改和 sessionVersion 失效测试。
4. 高优先级问题
CQ-API-001 / P1:API 缺少统一运行时输入校验
api/src/main.ts:23-27创建应用并配置 body parser,但没有注册全局ValidationPipe。- 搜索未发现
@IsString、@IsInt等class-validator规则。 - 统计到约 166 个控制器
@Body()入口;多数 DTO 是 TypeScript interface 或内联类型,运行时会被擦除。 api/src/open-api/open-api.dto.ts:3-14仅包含 Swagger 装饰器,没有输入校验装饰器。
风险包括错误类型进入服务层、超长字符串、额外字段、枚举外状态和不一致的 400/500 响应。建议建立 DTO class、transform: true、whitelist: true、forbidNonWhitelisted: true 的统一策略,并分批为高风险写接口补齐字段长度、枚举、数组大小和格式限制。
CQ-AUTH-001 / P1:验证码和匿名失败计数为进程内无界 Map
api/src/auth/auth.service.ts:20-21使用两个模块级Map。createCaptcha()会持续写入记录;过期记录只有在同一 captchaId 被再次提交时才删除。- 匿名失败以任意登录名为 key 写入,没有容量限制或周期清理。
这会造成多实例登录状态不一致,并可通过大量验证码请求或随机登录名制造内存增长。建议迁移到 Redis,使用 TTL、原子计数、IP+账号双维度限流和固定容量保护。
CQ-FE-001 / P1:前端主包过大且没有路由级代码分割
当前生产构建结果:
- JavaScript:2,123.63 KB,gzip 626.18 KB。
- CSS:270.61 KB,gzip 39.84 KB。
- Vite 明确产生“chunk 大于 500 KB”警告。
src/routes/AppRoutes.tsx:2-59同步导入全部运营端和客户端页面;代码中未发现React.lazy()或动态import()。src/components/ui/Chart.tsx:3使用import * as echarts from 'echarts',进一步扩大主包。
建议先按 admin/client 及页面路由使用 React.lazy + Suspense 分包,再按需引入 ECharts 图表模块。应在 CI 增加 bundle budget,例如初始 JS gzip 不高于 250 KB、单异步 chunk gzip 不高于 180 KB;实际阈值可在首轮拆包后校准。
CQ-TEST-001 / P1:测试覆盖和验收层级不足
- API:45 套、532 项通过;全源覆盖率为 statements 59.61%、branches 50.99%、functions 60.33%、lines 62.35%。
api/jest.config.cjs没有coverageThreshold。src/下前端*.spec.*/*.test.*文件数量为 0。- Gateway 内部包覆盖率约 49.7% 至 100%,但
cmd/gateway只有 0.7%,另两个命令包为 0%。 - 本轮测试没有启动真实 PostgreSQL、Redis、MinIO 或 CMPP 模拟器,不能证明真实后端闭环。
建议先为租户隔离、认证、账务、发送幂等、回执关联建立真实 PostgreSQL/Redis 集成测试;前端至少覆盖登录、权限、加载/空/错误、筛选分页和高风险确认操作;随后逐步设置覆盖率门槛,避免一次性追求无意义的高百分比。
5. 中优先级问题
CQ-DEP-001 / P2:生产依赖仍有 2 个 high advisory
pnpm audit --prod --json 返回:
react-router 7.18.1:GHSA-qwww-vcr4-c8h2,修复版本>=7.18.2。nanoid 3.3.16:GHSA-2v37-7h3g-55p8,修复版本>=3.3.18。
现有 security:verify 已证明项目没有使用 React Router RSC 模式,并验证了既有 PostCSS 缓解,因此当前可利用面低于审计工具的原始 high 评级;但版本仍处于公告范围,不能长期依赖“功能未使用”作为供应链治理。建议升级后重新执行构建、API 测试、前端浏览器回归和安全校验。
CQ-MAINT-001 / P2:超大文件与缺失静态风格门禁
api/src/send-chain/send-inbound-entry.service.ts约 84 KB。api/src/send-chain/send-gateway-submit.service.ts约 52 KB。src/apps/admin/AdminDownstreamDeliveriesPage.tsx约 44 KB。- 根项目和 API 均没有 lint/format 脚本,也未发现 ESLint/Prettier 配置。
建议先按“持久化、状态机、队列 claim、路由、计费”拆分 send-chain 服务;页面按筛选、表格、详情和恢复操作拆分。拆分时要求行为不变,并依靠当前测试防回归。
CQ-CONFIG-001 / P2:数据库连接存在硬编码开发默认凭据
api/prisma.config.ts:8-11。api/src/prisma/prisma.service.ts:39-41。
当 DATABASE_URL 缺失时,进程会尝试使用 cmpp/cmpp_password 连接本机数据库。建议生产角色 fail closed;仅在显式 NODE_ENV=development 或专用本地配置下允许开发默认值。
CQ-ARCH-001 / P2:真实 API 代码仍与 mock 目录耦合
src/mock/保留完整 localStorage mock service。src/apps/admin/auditColumns.tsx:3的生产组件仍从@/mock导入业务类型。
当前没有证据表明 mock service 仍被页面运行时调用,但该目录和类型依赖会误导后续开发,并增加重新接入静态数据的风险。建议把共享类型迁移到 src/api/types 或 domain 模块,并在构建/检查中禁止 src/apps/** 导入 src/mock/**。
CQ-REPO-001 / P2:仓库产物和审计基线不稳定
api/tsconfig.build.tsbuildinfo已被 Git 跟踪,每次构建产生无业务意义的修改。.gitignore没有统一忽略*.tsbuildinfo、outputs/和任务临时文件。- 最终工作区仍存在
=,outputs/,pnpm-lock.yaml,tmp_generate_ui_drafts.py等既有未跟踪内容;审计窗口内还曾出现随后被并发流程处理的临时部署脚本。 - 审计开始时 HEAD 为
4d4c1f3,期间其他会话先后提交了业务代码a70e9e2和部署记录171c7d3;本报告已对新业务代码重新运行前端类型检查和生产构建,但完整 API 覆盖率运行发生在该纯前端提交之前。
建议统一包管理器与唯一锁文件,忽略纯构建缓存,并在正式审查/发布时使用固定 commit 或独立 worktree,避免审计结论对应移动目标。
6. 已通过门禁
| 检查项 | 结果 |
|---|---|
前端 TypeScript tsc --noEmit |
通过;并发提交后已重跑 |
| Vite 生产构建 | 通过;存在包体警告 |
| API TypeScript 正式构建 | 通过 |
| API Jest | 45/45 套、532/532 项通过 |
| API 覆盖率 | statements 59.61%,branches 50.99%,functions 60.33%,lines 62.35% |
Gateway go test ./... -count=1 |
通过 |
Gateway go vet ./... |
通过 |
Gateway go test ./... -cover |
通过;各包覆盖率差异较大 |
| Prisma Schema | 95 个迁移目录;prisma validate 通过 |
| Gateway 队列契约 | 5/5 样例通过 |
| 依赖缓解/安全部署脚本 | 通过 |
pnpm audit --prod |
不通过;2 个 high advisory |
git diff --check |
通过 |
7. 积极发现
- PostgreSQL 连接按 API、Worker、Outbox、Callback 和 Protocol Log 角色设置独立连接池上限。
- 账务、日配额、频控等竞争资源使用事务级 advisory lock。
- 多个队列 claim 使用
FOR UPDATE SKIP LOCKED,适合并发消费者。 SmsMessageRecord、SmsBatchTask、CmppDownstreamDelivery等高频表具备 tenant/status/time 复合索引。- Webhook 主链对协议、内网地址、环回地址和 DNS 解析做了 SSRF 防护,并在 HTTP 客户回调中固定解析后的目标地址。
- 会话 cookie 使用 HttpOnly、SameSite=Lax,并按环境控制 Secure;前端 localStorage 保存的是会话展示元数据,不是 session token。
- API、Gateway 和队列契约测试已经覆盖大量发送、补发、回执去重和异常分支。
8. 未验证边界
本报告不能替代以下验证:
- 真实 PostgreSQL 数据量下的
EXPLAIN (ANALYZE, BUFFERS)和慢查询分析。 - Redis Stream/BullMQ pending、lag、重试、宕机恢复和重复消费验证。
- MinIO 上传、下载、租户隔离和大文件边界。
- 登录后的真实浏览器 UI、控制台、网络请求、加载/空/错误/刷新状态。
- Gateway 与 CMPP 模拟器或真实供应商的 Submit、长短信、回执、上行和断线恢复闭环。
- 测试、预生产、生产当前部署 commit、迁移数、配置和服务状态。
在完成 P0 修复前,不建议通过真实环境攻击性测试证明越权;应先补自动化隔离回归,再在隔离测试环境验证。
9. 建议整改顺序
第一批:发布阻断
- 服务端统一可信租户上下文,关闭请求头/请求体决定客户端租户的能力。
- 迁移密码哈希到 Argon2id/scrypt,并提供旧哈希透明升级。
- 补 tenant-a/tenant-b 越权测试和密码迁移测试。
第二批:安全与质量门禁
- 建立全局运行时 DTO 校验。
- 将验证码、失败计数和限流迁移到 Redis TTL/原子计数。
- 升级两个公告依赖并保持安全校验通过。
- 在 CI 增加 lint、format check、覆盖率门槛、依赖审计和 bundle budget。
第三批:性能与可维护性
- 前端路由级分包并按需加载 ECharts。
- 拆分 send-chain 超大服务和超大页面。
- 清理 mock 类型耦合、构建缓存和临时产物治理。
- 在隔离真实后端完成 API/DB/Redis/MinIO/Gateway/CMPP 全链回归。
10. 验收出口标准
整改完成至少应满足:
- P0 为 0,P1 有明确关闭证据或书面风险接受。
- tenant-a 用户无法通过 header、body、query 或资源 ID 访问 tenant-b 数据。
- 新密码使用强哈希;旧 SHA-256 账号登录后自动升级,数据库不再新增 SHA-256 密码。
- 前端初始 JS 包达到约定预算,核心路由按需加载。
- API/Gateway/前端测试与构建全部通过;覆盖率不低于本报告基线且建立门槛。
- 依赖审计不再包含本报告两项 high advisory。
- 真实 PostgreSQL、Redis、MinIO、Gateway 和 CMPP 测试证据与
docs/testing-progress.md同步。