GuDingZiChan/docs/2026-10-09_项目问题与隐患审查.md

140 lines
19 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.

# 2026年10月9日 固定资产系统问题与隐患审查
本轮列出 26 项整改问题:11 项 P1、15 项 P2,另有 3 项依赖配置或历史数据的条件性风险。优先处理误操作资产、虚假可用库存、HR 空源停用、打印状态一致性,以及本地修复未上线的问题。
代码基准为 `main@843babd` 加当前未提交工作区,包含 10 月 7 日的本地整改。生产证据采自 10 月 9 日固定资产系统的只读核查。本轮新增审计文档,未修改业务代码、数据库数据或部署,未补发消息。
P1 表示可能造成错误资产操作、数据损坏、核心流程中断或明显安全风险,应优先整改;P2 表示影响一致性、安全、恢复能力和使用体验。代码缺陷与“已经在线上造成损失”分别判断,后者不能仅凭 mock 推断。
## 当前生产状态
| 项目 | 10 月 9 日核查结果 | 含义 |
| --- | --- | --- |
| 维修快照 | `maintenance_record.original_tree_snapshot` 列不存在 | 本地快照整改尚未在生产生效 |
| 健康检查 | 已部署文件中没有新 `healthController.js`;健康接口仍固定返回 `Server is running` | HTTP 200 不能证明业务表、迁移和数据库可用 |
| 数据库 | 应用使用 root;3306 绑定宿主机 IPv4 和 IPv6 全网卡 | 最小权限和端口收敛尚未完成;当前电脑直连 3306 超时,不能据此断言公网已开放或所有来源都被阻断 |
| 运行时 | Node.js `v20.20.2`,进程 UID 为 0,时区 UTC,MySQL 默认隔离级别 `REPEATABLE-READ` | 日期偏移条件已具备;并发审查需考虑旧快照;运行账号权限过大 |
| 通知 | 通知中心 70 条已读、12 条未读,没有 failed;另有 5 条旧通道停用记录 | 暂无新的通知中心失败;历史旧通道记录不应当作需要补发的新故障 |
| 证书 | 严格 TLS 校验通过,到期为北京时间 2027 年 3 月 23 日 07:59:59 | 上次证书替换有效,当前没有过期问题 |
| 打印 | 162 条完成、15 条排队,最早排队任务创建于 3 月 30 日 | 存在长期积压;排队状态本身不证明已领取任务卡死 |
| 备份权限 | 最新标准备份目录及所有祖先目录为 755,数据库和上传归档为 644 | 同服务器其他本地账号可读取备份 |
生产当前不使用公开默认数据库密码。本轮不把上次已消除的默认密码问题再次认定为现存事实。
## P1 问题
### 1 维修和健康检查的本地整改尚未上线
生产没有维修快照字段,也没有新健康检查控制器;已部署维修、报废控制器与本地构建文件的 SHA256 不同。本地构建通过并不等于生产使用了这套代码,至少维修快照及真实健康检查仍未生效。
应把已完成的本地修复纳入可审查的发布版本,执行备份、迁移预检、增量迁移和发布验收。历史维修记录不能推测回填快照。相关文件:[维修控制器](<D:/AI Code/GuDingZiChan/backend/src/controllers/maintenanceController.ts:375>)、[健康控制器](<D:/AI Code/GuDingZiChan/backend/src/controllers/healthController.ts:1>)、[快照迁移](<D:/AI Code/GuDingZiChan/backend/prisma/migrations/20261007_maintenance_original_tree_snapshot/migration.sql:1>)。
### 2 切换资产详情后显示旧资产却操作新资产
详情只在 `onMounted` 加载,没有监听路由 ID;同组件复用时,页面继续显示资产 A,删除和维修却取新 URL 中资产 B 的 ID。顶部全局搜索从 A 进入 B 即可触发,存在删除错误资产、错误登记维修和保存错位资料的风险。
Vue 内存路由复现结果:URL 为 `/equipment/edit/2`,显示 ID 为 1,删除目标为 2,加载次数仍为 1。应在 ID 变化时重载并清空旧状态,操作目标与当前展示对象保持一致。[详情加载](<D:/AI Code/GuDingZiChan/frontend/src/views/equipment/EquipmentForm.vue:1312>)、[删除目标](<D:/AI Code/GuDingZiChan/frontend/src/views/equipment/EquipmentForm.vue:1251>)、[路由容器](<D:/AI Code/GuDingZiChan/frontend/src/layouts/MainLayout.vue:124>)。
### 3 损坏或丢失归还仍增加可用库存
归还状况只记在借用单,库存更新统一加 1 并恢复“可用”,没有让损坏或丢失进入对应资产状态。内存 mock 中,两种归还均成功,可用库存变为 1、可用价值恢复,借用单却仍记录损坏或丢失。
应先确定损坏、丢失的正式业务状态,再统一普通和批量归还规则;不能用备注替代库存状态。[归还状态计算](<D:/AI Code/GuDingZiChan/backend/src/services/assetMovementService.ts:824>)、[普通归还](<D:/AI Code/GuDingZiChan/backend/src/controllers/borrowController.ts:335>)、[批量归还](<D:/AI Code/GuDingZiChan/backend/src/controllers/batchController.ts:78>)。
### 4 维修中卸载原损坏配件会直接变可用
“维护中”父资产允许卸载,卸载后的子树只能设置为“可用”,没有核查未完成维修单及送修前状态。原损坏配件整树送修后被拆下,会绕过维修成为可借资产;之后取消维修又因树关系变化返回 409。
内存 mock 已复现父资产仍维护中、配件脱离后可用。应限制维修期间的资产树变更,或建立可审计的维修拆装流程并保留配件状态。[允许状态](<D:/AI Code/GuDingZiChan/backend/src/controllers/mountController.ts:20>)、[拆下后状态](<D:/AI Code/GuDingZiChan/backend/src/controllers/mountController.ts:38>)、[卸载入口](<D:/AI Code/GuDingZiChan/backend/src/controllers/mountController.ts:294>)。
### 5 完成维修可能覆盖已卸载并重新借出的配件
完成维修没有锁定整棵资产树,随后对子项仅按 ID 覆盖状态。在可重复读事务先建立快照、另一事务卸载并重新借出配件的交错下,完成事务可能从旧快照读出原配件,将其覆盖为可用,而新借用单仍未归还。
已用内存 mock 模拟该交错;生产默认隔离级别确为 `REPEATABLE-READ`,但尚未做隔离实库并发验证。应采用一致的整树锁顺序,并在写入时核对关系、状态和版本。[维修完成](<D:/AI Code/GuDingZiChan/backend/src/controllers/maintenanceController.ts:300>)、[子项无条件更新](<D:/AI Code/GuDingZiChan/backend/src/utils/assetValue.ts:171>)。
### 6 HR 成功空源可能停用全部同步人员
源查询成功但人员结果为空时,流程仍用 `source_userid notIn []` 停用所有同步人员,再删除没有借用或盘点历史的人员。上游 ETL 空窗、错误筛选和异常数据都可能被误认为人员整体离职。
原函数内存调用已捕获空源仍执行停用和清理的条件;未据此认定线上已经发生。应在写入前校验源完整性、空集和人数骤减,并把大批停用作为需要复核的变更。[批量停用](<D:/AI Code/GuDingZiChan/backend/src/services/hrSyncService.ts:1664>)、[执行顺序](<D:/AI Code/GuDingZiChan/backend/src/services/hrSyncService.ts:1838>)、[清理删除](<D:/AI Code/GuDingZiChan/backend/src/services/hrSyncService.ts:1267>)。
### 7 打印状态并发回写可把完成任务重新排队
打印回写先读取并检查状态,随后只按任务 ID 更新。完成与可重试失败同时读到 `printing` 时,完成先提交,失败仍可把任务改回 `queued`;完成路径还可能已清理标签图片。
mock 最终状态为 `queued`,两次更新条件都只有 ID。应使用包含原状态、领取终端和版本的条件更新,保持完成状态不可倒退,并协调图片清理。[回写入口](<D:/AI Code/GuDingZiChan/backend/src/services/printService.ts:1725>)、[更新条件](<D:/AI Code/GuDingZiChan/backend/src/services/printService.ts:1818>)。
### 8 打印领取后缺少失联恢复
领取后终端断网或崩溃,任务停在 `claimed/printing`;领取入口只读取 `queued`,管理员也只能取消 `queued`,没有租约到期回收或领取任务的人工核实入口。
应增加领取租约和心跳、失联状态及人工恢复入口。对可能已经出纸的任务,恢复时必须区分结果未知和明确未打印,避免盲目重新排队。[领取](<D:/AI Code/GuDingZiChan/backend/src/services/printService.ts:1614>)、[取消限制](<D:/AI Code/GuDingZiChan/backend/src/services/printService.ts:1390>)、[任务模型](<D:/AI Code/GuDingZiChan/backend/prisma/schema.prisma:234>)。
### 9 .NET 打印助手把结果回写失败误报为打印失败
实际打印和 `completed` 请求位于同一个 try 中。打印已提交,但完成回写请求未到服务器时,catch 又上报 `failed`,使“已打印但回写未知”变成“打印失败”;管理员补发可能重复出纸。
应分别处理实际打印与状态回写,持久化待回写结果,重启后继续对账。[打印与完成回写](<D:/AI Code/GuDingZiChan/print-agent-dotnet/FixedAssetPrintAgent/PrintAgentRuntime.cs:365>)、[失败回写](<D:/AI Code/GuDingZiChan/print-agent-dotnet/FixedAssetPrintAgent/PrintAgentRuntime.cs:381>)。此项为代码控制流证据,未用实体打印机触发。
### 10 本地登录缺少限流且允许极弱密码
应用的本地登录没有账号或 IP 的失败计数、退避和锁定;后端创建及重置密码只要求非空,单字符密码可被接受。生产当前使用 local 模式,因此该登录路径启用;是否另有外围 WAF 限流没有核实。
应在应用层建立密码强度规则及登录限流,保留审计,同时避免把攻击者的失败尝试变成永久锁死正常账号。[登录](<D:/AI Code/GuDingZiChan/backend/src/routes/auth.ts:49>)、[重置校验](<D:/AI Code/GuDingZiChan/backend/src/controllers/userController.ts:461>)。
### 11 发布覆盖旧源码后才做部署备份
发布脚本先删除并替换服务端 backend、frontend、scripts 和配置,再调用 deploy.sh 中的“发布前备份”。因此标准备份保存的是新配置,旧源码已被删除;后续构建或上线失败没有自动回到上一版本的步骤。
应在覆盖前保存旧版本和配置,保留可回退的镜像标识,再采用原子切换;发布失败需有明确恢复路径。[先覆盖文件](<D:/AI Code/GuDingZiChan/scripts/publish-to-server.ps1:269>)、[后调用部署](<D:/AI Code/GuDingZiChan/scripts/publish-to-server.ps1:315>)、[备份配置内容](<D:/AI Code/GuDingZiChan/scripts/backup-equipment.sh:104>)。
## P2 问题
| 编号 | 问题与影响 | 证据与整改方向 |
| --- | --- | --- |
| 12 | 重置密码不撤销旧会话;被盗会话可在改密后继续使用 | [密码更新](<D:/AI Code/GuDingZiChan/backend/src/controllers/userController.ts:470>)仅改密码,[会话认证](<D:/AI Code/GuDingZiChan/backend/src/middleware/auth.ts:27>)不校验认证版本。mock 确认旧会话仍通过。引入认证版本或按用户撤销会话 |
| 13 | 会话过期后,前端在登录与首页之间循环 | [路由守卫](<D:/AI Code/GuDingZiChan/frontend/src/router/index.ts:183>)捕获失败但不清 Pinia 认证状态。真实守卫提取复现循环。统一 401 和会话失效的清理流程 |
| 14 | 跨页批量借出和归还静默漏掉前页所选资产 | [编码查询](<D:/AI Code/GuDingZiChan/frontend/src/views/equipment/EquipmentList.vue:1437>)只查当前页,选中 ID 却跨页保留。源码提取复现只提交第 2 页资产。维护完整选中对象或按 ID 批量获取 |
| 15 | 预计归还时间在生产偏移 8 小时,影响逾期判断 | [前端提交](<D:/AI Code/GuDingZiChan/frontend/src/views/borrow/BorrowScan.vue:566>)传无时区日期,[后端解析](<D:/AI Code/GuDingZiChan/backend/src/controllers/borrowController.ts:35>)按服务器时区解释。生产为 UTC;北京时间 12 点会保存为 12:00Z,回显 20 点。提交含明确时区的 ISO 时间 |
| 16 | Excel 数量 0 或非数字被改成 1,错误行被丢弃且行号错位 | [导入转换](<D:/AI Code/GuDingZiChan/frontend/src/views/equipment/EquipmentList.vue:1531>)使用默认数量和 filter。源码提取复现 3 行仅提交 2 行。保留原始行号,逐行报告错误,禁止静默改数 |
| 17 | 已关闭盘点任务仍可分配责任人,改动已结束结果 | [分配入口](<D:/AI Code/GuDingZiChan/backend/src/services/inventoryService.ts:1743>)查询任务仅选 ID,未核查状态。closed 任务 mock 分配成功。事务内校验并锁定任务状态 |
| 18 | 不同参与人并发盘点会覆盖汇总计数 | [汇总](<D:/AI Code/GuDingZiChan/backend/src/services/inventoryService.ts:704>)先聚合后覆盖,[调用](<D:/AI Code/GuDingZiChan/backend/src/services/inventoryService.ts:1524>)缺少任务级串行保护。mock 明细 checked=2 而汇总为 1;尚未隔离实库验证。采用一致锁序与正确隔离读,或安全增量计数 |
| 19 | 借出时无配件的普通归还跳过后来新增配件的确认 | [差异检查](<D:/AI Code/GuDingZiChan/backend/src/controllers/borrowController.ts:274>)遇 null 快照直接认为无差异。mock 更新根和新配件却不要求确认。区分可靠空集合与缺失快照,对当前资产树做完整比对 |
| 20 | 打印图片只校验 Data URL,缺少真实 PNG、尺寸和单任务数量限制 | [校验](<D:/AI Code/GuDingZiChan/backend/src/services/printService.ts:495>)接受伪 PNG,后续落盘;[请求解析](<D:/AI Code/GuDingZiChan/backend/src/index.ts:61>)在认证前允许 50 MB。应限制解码字节、像素和标签数量,并适度限制公开端点请求体 |
| 21 | 数据库运行账号权限过大,生产端口仍绑定全网卡 | [Compose](<D:/AI Code/GuDingZiChan/docker-compose.yml:32>)仍使用 root;生产实际映射 3306 与 3000。应用故障或入侵会扩大数据库影响面;公网直连未证实。分离迁移与运行账号,取消无必要端口暴露 |
| 22 | 生产数据库及上传备份可被其他本地账号读取 | 最新标准备份数据文件 644,全部父目录 755;[备份脚本](<D:/AI Code/GuDingZiChan/scripts/backup-equipment.sh:88>)无限制 umask。设置目录 700、文件 600,核查并修正存量归档权限 |
| 23 | 标准备份不足以独立恢复整套服务 | [备份内容](<D:/AI Code/GuDingZiChan/scripts/backup-equipment.sh:104>)不含环境配置、证书、对应版本源码或镜像。只持有标准备份时不能独立重建服务。分开保管加密配置和证书,记录版本及镜像,并做隔离恢复演练 |
| 24 | KEEP_BACKUPS=0 被接受并删除全部标准备份,包括刚生成的一份 | [参数校验](<D:/AI Code/GuDingZiChan/scripts/backup-equipment.sh:25>)接受 0,[清理](<D:/AI Code/GuDingZiChan/scripts/backup-equipment.sh:45>)删除全部。纯 mock 替换 rm 后已复现。要求正整数并在清理前确认至少保留一份完整备份 |
| 25 | 容器缺少内存限制和日志轮转,可能耗尽共享主机资源 | 生产 equipment 容器 Memory=0,json-file 日志配置为空;[Compose](<D:/AI Code/GuDingZiChan/docker-compose.yml:1>)没有资源或日志限制。应按实际容量配置限额和 max-size/max-file,并监测磁盘;本轮未证明已经耗尽 |
| 26 | 依赖与运行时需要安全更新 | 本地生产依赖扫描后端报告 27 项包级告警、前端 10 项;告警数不等于可利用漏洞数。xlsx 0.18.5 在实际导入路径被使用,生产 Node 20 已处于官方 EOL。需逐条判断可达性、升级并回归;不要盲目采用 audit 建议的跨主版本降级 |
第 26 项的直接证据:[xlsx 锁定版本](<D:/AI Code/GuDingZiChan/frontend/package-lock.json:3958>)、[实际读取文件](<D:/AI Code/GuDingZiChan/frontend/src/views/equipment/EquipmentList.vue:1513>)。SheetJS 官方确认读取特制文件时的[原型污染](https://cdn.sheetjs.com/advisories/CVE-2023-30533)及[正则拒绝服务](https://cdn.sheetjs.com/advisories/CVE-2024-22363)问题覆盖该版本。Node.js 官方[版本状态表](https://nodejs.org/en/about/previous-releases)已将 Node 20 标为 EOL。
## 条件性风险
1. **Keycloak 用户名映射可能继承本地高权限身份。** [身份映射](<D:/AI Code/GuDingZiChan/backend/src/utils/authIdentity.ts:93>)按用户名匹配,没有绑定 issuer 和 subject。只有启用 Keycloak 且非可信用户能注册或改成对应本地用户名时才成立。当前生产是 local 模式,不能说线上已发生该越权。
2. **旧 legacy 盘点共享链接可选择他人身份。** [旧身份确认](<D:/AI Code/GuDingZiChan/backend/src/services/inventoryService.ts:1404>)依赖 token 和参与人 ID。当前生产唯一 legacy 任务已关闭,活跃任务为 dingtalk 模式;若将历史任务重新开放或恢复为可操作状态,需要先迁移身份验证。
3. **打印助手允许 HTTP 携带长期令牌。** [Electron 地址配置](<D:/AI Code/GuDingZiChan/print-agent/src/config/storage.js:32>)和 [.NET 地址配置](<D:/AI Code/GuDingZiChan/print-agent-dotnet/FixedAssetPrintAgent/PrintAgentRuntime.cs:482>)未强制 HTTPS,客户端随后附带 Bearer。纯 mock 已证实此行为;默认 HTTPS 不触发,当前实际终端配置未核实。生产地址应拒绝 HTTP,开发例外单独控制。
## 修复顺序与验收
1. 先处理资产详情错位、损坏或丢失归还、维修拆装与完成并发、HR 空源保护。验收必须覆盖错误资产 ID、损坏库存、树关系变更、并发借出以及空源和人数骤减。
2. 修正打印回写条件、失联恢复和 .NET 结果回写,区分已打印、未打印、结果未知;增加持久化恢复和重复上报场景。
3. 补齐密码规则、登录限流及改密撤销会话,同时修复前端 401 循环。
4. 收敛备份权限、修正覆盖前备份和回退流程,再把已有本地维修、权限、健康检查及部署整改发布到生产。新增数据库字段走增量迁移,不自动猜测旧记录。
5. 处理跨页选择、日期时区、导入错误行、盘点状态和计数,以及依赖与容器资源配置。并发问题在隔离 MySQL 环境验证,不向生产注入测试数据。
## 已执行验证及范围
- 前端 `npm run build`、后端 `npm run build` 均通过。
- 后端现有 251 项测试通过;执行时数据库地址设为不可用的本机审计地址,避免连接真实数据库。
- Electron 打印助手现有 18 项测试通过。
- 备份保留、报废迁移预检、数据库部署配置 3 个 shell 测试脚本分别通过。
- 资产路由复用、跨页选择、认证循环、导入转换,以及归还、维修、HR、打印、盘点相关触发路径使用源码提取或内存 mock 验证。并发 mock 不能替代隔离实库验证,具体限制已随条目标明。
- 生产只读核查限定固定资产目录、equipment 容器及 equipment_management 数据库,未操作同机其他系统。
本轮未完成实体打印验收、带真实登录态的浏览器流程验收和隔离实库并发压测。现有测试通过说明已覆盖场景正常,不能排除清单中的未覆盖分支。前端构建中统计相关共享 chunk 约 1.12 MB,移动端加载性能也值得进一步测量;构建体积本身不证明页面已经卡顿。