140 lines
19 KiB
Markdown
140 lines
19 KiB
Markdown
# 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,移动端加载性能也值得进一步测量;构建体积本身不证明页面已经卡顿。
|