# LAN 网关安全评估 评估日期:2026-09-06。对象:`@riceawa/dsh-lan-gateway` 0.4.0,提交 `77755f6`。 > **修复状态(0.5.0,2026-09-06)**:本报告的 F1–F5 已在 v0.5.0 按 > [修复方案](qvd-2026-57410-fix-plan.md)(§15 实施状态)落地并通过 72 项回归 > (默认全来源认证、共享上游会话中继、会话代次撤销、加密入口 fail-closed 等)。 > 本文件保留为审计时的原始结论快照,行文仍按「审计当时(0.4.0)」的口径。 ## 结论 应取消 LAN、link-local 和 loopback 免密,改成所有来源默认拒绝、所有上游请求先鉴权。 这不是简单调整 `lanCidrs` 默认值:当前回环地址和部分 IPv6 link-local 地址有独立的免密分支。 `authRequired: true` 当前也只约束被分类为 `internet` 的来源。 插件默认 `enabled: false`,未开启监听时不存在网关网络入口。以下网关风险以监听已启用为前提; 原生 DSH 上注册的 `/lan-gateway/config` 则不依赖网关监听是否开启。 本轮仅调研、代码审计和隔离验证,不修改生产源码、真实设置、密码或运行中的服务。 没有向实际 DSH API 发请求,没有调用模型或执行 RCE 载荷。 ## 与 QVD-2026-57410 的关系 - 公共资料将该编号描述为 DSH Web API 的 Host 信任判断缺陷,披露的受影响版本包含 `0.1.1-rc.2`。 - Host 是请求目标地址的 HTTP 字段,不是客户端身份。能到达 Web 服务的非浏览器客户端可以自行设置它;Origin 和 Fetch Metadata 也不能代替客户端认证。 - “不需要有效 API Key”不能理解成“没有配置模型 Key 就安全”。模型供应商凭据和访问本地 Harness 的身份凭据是两种不同的权限边界。 - 本插件的入口来源分类确实读取 `socket.remoteAddress`,不使用 `Host` 或 `X-Forwarded-For`。隔离验证中,公网来源仅伪造这两个头仍收到 `302 /__login`,没有穿透网关登录。 - 但 LAN/loopback 一旦被免密放行,网关主动把 Host/Origin 改成回环地址,替来访者通过旧版 DSH 的信任检查。攻击者甚至不必自己伪造 Host。 - 本插件自己的配置接口重复了 Host-only 信任判断,这是独立于上游 `/api` 修复状态的管理面风险。 上游公告、修复提交与版本核查见 [漏洞调研](qvd-2026-57410-research.md)。 不可把 Context7 中不同时间的文档片段拼成同一版本的实现,也不可把“公开讨论中的 PoC”当作维护者正式验证。 ## 发现 ### F1:高危,来源网段直接获得未认证的上游访问 位置:`src/gateway.ts:175`、`src/gateway.ts:290`、`src/auth.ts:91`。 只有 `internet && authRequired` 才验 Cookie。默认 RFC1918、部分 link-local、回环请求都跳过认证, HTTP 与 WebSocket 都如此。网关随后在 `src/gateway.ts:260`、`src/gateway.ts:299` 重写上游 Host。 影响包括同一网络的其他用户、被攻陷设备,以及把外部请求转接为私网或回环连接的反代、隧道、某些容器网络部署。 例如外部请求经本机反代连接网关,网关看到的是反代的回环 IP,而不是真实外部用户。 这不要求伪造 TCP 来源地址,也不因忽略 X-Forwarded-For 而消失。 隔离验证:没有任何 Cookie 或 Authorization 时,回环来源与模拟 LAN 来源均获上游 `200`, 模拟公网来源获 `302`。任意匹配的 Host/Origin 也会被 LAN 路径接受,网关没有服务域名白名单。 若上游是缺少独立身份认证的受影响 DSH,其敏感操作将暴露;本轮没有运行完整 RCE 链。 ### F2:高危,配置接口没有身份鉴权,可被未认证访问者修改安全策略 位置:`src/index.ts:208`、`src/index.ts:327`、`src/index.ts:386`、`src/index.ts:403`。 `/lan-gateway/config` 只要求 Host 看起来是回环地址;缺少 Origin 时直接通过。 它没有检查网关会话、独立管理凭据或实际 socket 来源。 通过 LAN 网关访问时,Host 已被网关重写,未登录即可读取配置。 设置服务可用时,同一个访问者可以提交 `authRequired: false`,配置被持久化并触发监听器重载, 进而把未认证访问范围从 LAN 扩大到所有能到达监听端口的来源。 隔离验证:配置 GET 返回 `200`;配置 POST 返回 `200`,模拟 settings 服务收到 `authRequired: false`。 真实设置完全未动。直接调用处理器并模拟公网 remoteAddress、提供回环 Host 也得到 `200`。 如果原生 DSH 端口被额外暴露,攻击者还能绕开网关直接访问此路由。原生端口仅绑定回环且未被转发时, 远程直连路径不可达;不能把处理器漏洞等同于本机当前已经公网暴露。 仅给上游 `/api` 加认证不自动保护这个位于 `/api` 之外的插件路由。 ### F3:高风险缺口,HTTP 与 WebSocket 的跨站检查不一致 位置:`src/gateway.ts:182`、`src/gateway.ts:195`、`src/gateway.ts:288`。 HTTP 的部分 API 路径会调用 `passesCsrfFence`;升级路径完全不调用它,反而直接重写 Origin。 隔离验证中,相同跨站头的普通 HTTP 请求被 `403` 拒绝,升级请求却到达模拟上游并收到 `101`。 实际能否劫持 DSH WebSocket 还依赖浏览器 Cookie/Fetch Metadata 行为和具体上游端点的独立校验。 本轮证明的是网关没有履行自身跨站防护,不宣称已完成浏览器端会话劫持。 另有路径覆盖风险:CSRF 判断只对原始字符串 `/api` 和 `/api/` 前缀生效。 带点段的 `/unused/../api/audit-read-only` 可以穿过网关这一检查;若上游规范化后将其路由到 API, 就会出现前后端检查不一致。本轮只验证了网关转发,未确认实际 DSH 路由和其额外防护,因此不单独判定为已证实利用链。 ### F4:高风险部署默认值,密码和会话可以通过明文 HTTP 传输 位置:`src/index.ts:135`、`src/gateway.ts:239`、`src/gateway.ts:245`。 TLS 默认关闭;登录表单提交真实密码,Cookie 仅在网关自己开启 TLS 时添加 Secure。 隔离验证的正常登录签发了不带 Secure 的 Cookie。HMAC 只防 Cookie 伪造,不防窃听和重放。 如果把 LAN 也改成必须登录,却继续让用户通过不可信网络上的 HTTP 登录,就会引入实际的凭据泄漏路径。 须同时采用 HTTPS,或具备身份认证和加密的可信传输。受信 TLS 终止代理的模式需要明确配置, 不能通过来访者可伪造的 X-Forwarded-Proto 自行判断连接安全。 ### F5:中风险,改密或清空密码不会撤销已签发会话 位置:`src/state.ts:50`、`src/index.ts:439`、`src/gateway.ts:134`。 密码变更保留 `cookieSecret`,会话验证只依赖 HMAC 和到期时间。 隔离验证确认旧 Cookie 在改密、清空密码后仍然有效。清空密码后的提示还声称非 LAN 会变成免密, 但实际代码会拒绝新的密码登录、继续接受未过期的旧 Cookie,并继续保留 LAN 免密。 应在改密/清空密码时轮换会话签名密钥或会话版本;清空凭据必须关闭入口或拒绝所有受保护请求。 已有 WebSocket 连接也需要定义撤销策略:当前 `setState` 只影响后续请求,`closeAllConnections` 不负责销毁升级后的 socket。 ## 验证结果 原有 `pnpm test`:4 个测试文件、39 项全部通过。但 `tests/gateway.test.ts` 只测分类、Cookie、密码和限流原语, 没有实例化真实网关,没有 HTTP/WS/配置路由集成测试,因此这些通过结果不能证明网关鉴权边界正确。 本轮额外完成 13 个隔离观察场景: | 场景 | 观察结果 | | --- | --- | | 回环来源,伪造 Host/XFF,无凭据 | 上游 200 | | 模拟 LAN 来源,伪造 Host/XFF,无凭据 | 上游 200 | | 模拟公网来源,伪造 Host/XFF,无凭据 | 网关 302,未转发 | | LAN,任意匹配 Host/Origin,无凭据 | 上游 200,Host/Origin 已变回环 | | LAN,跨站 HTTP API | 网关 403 | | LAN,同样跨站头,WS 升级 | 模拟上游 101 | | LAN,跨站头,含点段 API 路径 | 到达模拟上游,实际 DSH 路由未验证 | | LAN,配置 GET,无凭据 | 200 | | LAN,配置 POST,无凭据 | 模拟 settings 收到关闭认证配置 | | 直接访问配置路由,改变 Host | 非回环 Host 403,回环 Host 200 | | 配置处理器,模拟公网 socket + 回环 Host | 200 | | 模拟公网,正确密码登录并携 Cookie | 登录 302,受保护请求 200,Cookie 无 Secure | | 原有 Cookie 在改密/清密后验证 | 均仍有效 | 验证使用临时脚本 `/tmp/dsh-gateway-security-audit.mts`,以仓库 `tsx` 执行。 全部监听实际绑定 `127.0.0.1` 随机端口;LAN/公网用测试 socket 属性注入模拟,未实施来源地址欺骗。 上游为只返回固定内容/握手的 HTTP 服务,配置持久化接口为内存替身,HOME 为临时目录。 所有临时监听均已关闭,临时 HOME 已清理。上述是现状确认,不是安全回归测试通过。 ## 建议目标 1. 取消 IP 免密。LAN/loopback 与公网采用同样的认证要求;CIDR 若保留,只作为认证之外的访问范围限制。 2. 鉴权覆盖所有转发路径:页面、静态资源、API、SSE、插件接口和所有 WebSocket upgrade;未知路径也先鉴权。 3. 唯一必要匿名例外是网关自己处理、绝不转发的登录页和登录提交;登录同样做来源检查、限流和体积限制。 4. 配置接口使用实际已认证的管理身份。不可依赖 Host,也不可仅信任反代留下的回环 IP。原生 DSH 入口上的管理路由必须有独立防护,或迁入已认证网关并保留受控本地恢复方式。 5. 共享 HTTP/WS 的安全决策;在 Host/Origin 重写之前完成检查。固定允许的服务 Host,比较完整 Origin(协议、主机、端口),Cookie 认证的写操作检查 CSRF;不要靠路径前缀遗漏其他插件接口。 6. 默认开启 TLS 或要求显式配置可信加密入口;密码未设置、状态损坏、认证初始化失败时拒绝开启监听,禁止失败后退回免密。 7. 旧 `authRequired: false` 和历史 LAN 免密设置不能悄悄延续到安全升级后。明确废弃免密模式,对不安全旧配置拒启并提供迁移提示,而非只更新新安装默认值。 8. 改密撤销旧会话,增加登出,并关闭需要撤销的长连接;给密码设置提供非模型对话输入通道,避免密码出现在聊天记录或工具审计轨迹。 9. 继续将原生 DSH 端口隔离在回环/受限网络内,不另开可绕过网关的转发入口。升级上游,并确认其身份认证确实启用;不要通过伪造本地身份去规避新版的真实登录。 “默认零信任”在这里指不以网段或 HTTP 头代替身份认证,并不意味着增加一个登录页就实现完整的多用户授权、审计或零信任架构。 ## 修复前处置 - 不建议把当前插件的登录页视为公网反代/隧道部署的可靠认证层。 - 暂时关闭网关网络暴露,或在外层加覆盖所有 HTTP 与 WebSocket 的独立身份认证和加密,同时阻断直连网关及原生 DSH 端口的路径。 - 不要用 `lanCidrs: []` 作为完成修复的标志;回环免密仍在。也不要认为 `authRequired: true` 当前等于全来源鉴权。 - 未检查本机监听、防火墙、真实 DSH 版本或历史入侵痕迹;本报告不证明实例已遭入侵,也不证明实例当前未暴露。