Files
Nexus/docs/changelog/2026-05-26-server-form-ux-bugfix-security.md
T
2026-07-08 22:31:31 +08:00

6.9 KiB
Raw Blame History

2026-05-26 — 服务器表单 UX 优化 + Bug 修复 + 安全加固

变更摘要

  1. 分类字段改为 select+input combobox — 替换不可见的 <datalist>,用 <select> 下拉 + __custom__ 自定义输入模式,用户可从已有分类选择或自由输入新分类
  2. Agent 端口隐藏srvAgentPort 改为 type="hidden",默认值 8601 不变,减少表单认知负担
  3. 目标路径默认值 — 从 /home/deploy/target 改为 /www/wwwroot
  4. CSV 模板修正 — 移除 Agent端口/平台/节点 列,更新示例行(target_path=/www/wwwroot, category=商城);后端保留 _CSV_IMPORT_ONLY_COLUMNS 向后兼容旧模板导入
  5. Bug 修复:showEditServer 分类同步 — 修复编辑服务器时分类值丢失的两个 bug:
    • _refreshCategoryDatalist() 在分类同步逻辑之后调用,导致 select 选项为空
    • 第二次冗余 _refreshCategoryDatalist() 调用会触发 _onCategoryChange() 清空自定义分类值
  6. XSS 加固agent_version 渲染加 esc() 转义;esc() 函数增加双引号转义(&quot;),防止 HTML 属性上下文注入
  7. XSS 加固(审计追加) — CSV 导入错误显示中 e.namee.domaine.rowesc() 转义;预设/SSH密钥预设 <option value>p.idesc() 防属性突破
  8. 空 catch 消除 — 4 处空 catch(e){} 全部添加 console.warn() 日志(Agent 轮询器、预设加载、SSH 密钥预设加载、剪贴板写入);后端 3 处 except Exception: pass 改为 logger.warning()CSV platform/node 加载、批量 Agent 回滚×2)
  9. Nginx 缓存控制 — 为 /app/*.js/app/*.css 添加 Cache-Control: no-store,防止浏览器缓存旧版静态文件
  10. Bug 修复:SSH 密钥双重加密update_server 中从 SSH 密钥预设解析密钥时预先 encrypt_value(),遍历 ALLOWED_FIELDS 时又加密一次,导致 ssh_key_private 被双重加密,WebSSH 连接报 Invalid private key。修复为与 create_server 一致:预设解析后存明文,统一在遍历步骤中单次加密

动机

  • <datalist> 在大多数浏览器中不显示下拉箭头,用户无法发现分类选项
  • Agent 端口极少修改,展示增加表单复杂度
  • /home/deploy/target 不是中国用户典型部署路径,/www/wwwroot 更符合宝塔面板习惯
  • CSV 模板包含用户不使用的列(Agent端口/平台/节点),造成困惑
  • 代码审计发现 showEditServer 中分类同步 bug 和 XSS 隐患

涉及文件

文件 改动
web/app/servers.html 分类 comboboxselect+input)、Agent 端口 hidden、目标路径默认值、showEditServer 分类 bug 修复、agent_version XSS 修复、esc() 双引号转义、CSV 导入错误 XSS 修复、预设 option value esc() 加固、4 处空 catch 加日志、移除冗余 _onCategoryChange() 调用
server/api/servers.py CSV_COLUMNS 重构(移除 Agent端口/平台/节点)、新增 _CSV_IMPORT_ONLY_COLUMNS 向后兼容、模板示例更新、3 处空 except 改 logger.warning()、SSH 密钥预设双重加密 bug 修复
Nginx 配置(远程) /app/*.js/app/*.css 添加 Cache-Control: no-store

Bug 详情

showEditServer 分类同步 bugHIGH

现象:编辑服务器时,若服务器分类不在已有选项中,分类值被清空。

根因

  1. _refreshCategoryDatalist() 在分类同步逻辑(614-618 行)之后调用 → select 选项为空时无法匹配
  2. 第二次 _refreshCategoryDatalist() 调用触发 _onCategoryChange(),当 sel.value==='__custom__' 时执行 inp.value='',覆盖了刚设置的自定义分类值

修复

  1. _refreshCategoryDatalist() 调用移到同步逻辑之前
  2. 删除冗余的第二次 _refreshCategoryDatalist() 调用
  3. showEditServer 中直接设置 select/input 状态,不调用 _onCategoryChange()

agent_version XSSMEDIUM

现象s.agent_version 未转义直接插入 HTML。

修复:改为 esc(s.agent_version)||'--'

esc() 双引号不转义(LOW

现象esc() 使用 textContent→innerHTML 方法,不转义双引号,在 <option value="${esc(c)}"> 属性上下文中可能被突破。

修复esc() 末尾追加 .replace(/"/g,'&quot;')

CSV 模板示例行列数不匹配(MEDIUM)

现象CSV 模板 header 有 11 列,但 example 行只有 10 个值。缺少 私钥内容ssh_key_private)列的空值。

根因:密钥路径和私钥内容都是空值,需要两个连续逗号 ,,,,但 example 行只写了一个 ,,

修复example 行从 your-password,,商城 改为 your-password,,,商城

SSH 密钥双重加密(HIGH

现象WebSSH 连接使用 SSH 密钥预设的服务器时,报 Invalid private key 错误。

根因update_server 中从 SSH 密钥预设解析密钥时执行 encrypt_value(decrypt_value(preset.encrypted_private_key)),将预设密钥解密后立即加密存入 update_data。随后遍历 ALLOWED_FIELDS 时,对 ssh_key_private 字段又执行了一次 encrypt_value(value),导致双重加密。create_server 路由无此问题(预设解析后存明文,统一加密步骤只执行一次)。

修复

  1. update_server 预设解析改为存明文(与 create_server 一致):decrypt_value(preset.encrypted_private_key)
  2. 已受影响的 server 8 数据修复:双重解密后重新单次加密

是否需迁移/重启

  • DB 迁移: 无
  • 需重启: 是(servers.py 后端改动需重启服务,已执行)
  • Nginx: 已添加 no-cache 头(需 reload,已执行)

验证方式

  1. 编辑「机器人」服务器(分类=商城)→ 分类下拉正确显示"商城"选中
  2. 编辑「冲量银海」服务器(无分类)→ 分类下拉显示"— 无分类 —"
  3. 添加服务器 → 分类默认"— 无分类 —"、目标路径 /www/wwwroot、Agent 端口 8601hidden
  4. 选择"✏️ 自定义输入…"→ 文本输入框出现,输入值正确同步到 srvCategory
  5. 切换回已有分类 → 自定义输入框隐藏,值正确同步
  6. CSV 模板下载 → 无 Agent端口/平台/节点 列,11列 header = 11列 example,含 /www/wwwroot
  7. esc() 函数转义双引号 → esc('test"val') 返回 test&quot;val
  8. 远程服务器 Nginx 为 JS/CSS 返回 Cache-Control: no-store
  9. 逐行审计 servers.html0 FINDING1 SAFE(布尔表达式)
  10. 逐行审计 servers.py0 FINDING,SQL/命令注入/敏感泄露/大规模赋值全 SAFE
  11. 空 catch 块:前端 0 处、后端仅保留 json.JSONDecodeError 特定异常
  12. 部署后验证:esc(p.id) 出现 2 次、console.warn 出现 4 次、空 catch(e){} 出现 0 次
  13. Server 8 SSH 密钥解密验证:单次 decrypt_value() 得到 -----BEGIN OPENSSH PRIVATE KEY-----