Bug: HTTP 403 on all /api requests in Chrome 150+ (port-less Origin vs host comparison) #3106
Replies: 2 comments
中文版说明 (Chinese version of the issue description)受影响版本: 现象Chrome 150 及以上版本打开 WebUI 后,页面发出的所有 本机模式( 根因Chrome 150 改变了 loopback 地址的 Origin 序列化行为:页面地址为 而 function isTrustedApiRequest(request, trustedHosts) {
...
const origin = header(request.headers, "origin");
if (origin === void 0) return true;
// 当前代码 (rc.7):
return new URL(origin).host === hostUrl.host; // "127.0.0.1" !== "127.0.0.1:3080" -> false
}
建议修复方案只比较 hostname,忽略端口。端口不是 CSRF / DNS-rebinding 的安全边界:跨站攻击者页面是不同的 hostname,DNS rebinding 改变的是 Host 头里的 hostname——第一道"Host 必须为 loopback 或受信任"的校验加上 // 推荐写法:
return new URL(origin).hostname === hostUrl.hostname;建议修改范围(4 个拒绝点都复用同一个
附带建议(局域网模式)顺手处理同一文件中的另一处问题:当服务以 修复验证改用 hostname 比较后:Chrome 150+ 下 感谢这个优秀的项目——这是我维护的"双击即用"绿色分发版唯一被卡住的问题。 本报告由 AI 辅助撰写(根因分析与修复建议已对照 npm 官方 tarball 核实)。 |
Update: a green "double-click to run" distribution already fixes this (中文/English)我们维护的绿色整合版启动器已经解决了这个问题,欢迎参考或直接使用(附上我们的方案与仓库地址)。 We maintain a green "double-click to run" launcher distribution that already fixes this issue. Feel free to reference the approach or use the release directly. Repos and solution below. 解决方案 (How we fixed it)启动器在安装/启动 dsh 时,会对 The launcher applies an idempotent patch to 段 1 · Chrome 150 无端口 Origin(本贴主问题) / Part 1 · port-less Origin (the issue in this thread): // 官方 rc.7 现状 / upstream rc.7:
return new URL(origin).host === hostUrl.host; // "127.0.0.1" !== "127.0.0.1:3080" -> 403
// 我们的补丁 / our patch:
return new URL(origin).hostname === hostUrl.hostname; // 忽略端口, Chrome 150+ 恢复 200段 2 · 局域网模式 trustedHosts / Part 2 · LAN mode trustedHosts:未显式配置 When binding 验证 / Verified:Chrome 150+ 下 绿色版仓库地址 (Repositories)绿色整合版特点:双击即用、所有运行时依赖(便携 Node / 内置 Python / dsh 本体)全部本地化在程序目录、不写用户主目录、不占 C 盘、整目录拷贝即用;对外围文件更新走绿色版自身的 Release(GitHub 连不通自动转 Gitee 发布版附件直连下载)。 The green edition is double-click-to-run with all runtimes (portable Node / bundled Python / dsh itself) localised inside the program directory — no writes to the user profile, no C-drive footprint, copy-and-run. Its own green-version updater falls back from GitHub to Gitee release attachments when GitHub is unreachable. 欢迎直接下载使用;如果本贴的修复被官方采纳,我们的补丁会自动停用对应段落。 Feel free to try it out. If upstream adopts a fix, our patch will automatically retire that part. 本回复由 AI 辅助撰写 / This reply was drafted with AI assistance. |
Uh oh!
There was an error while loading. Please reload this page.
Bug: HTTP 403 on ALL /api requests in Chrome 150+ (port-less Origin vs. host comparison)
Affected version: @deepseek-ai/dsh-client-connection
0.1.0-rc.7(verified from the published npm tarball on 2026-08-18) — the issue is still present in the latest release.Symptom
When the WebUI is opened in Chrome 150 or newer (this is the version where the behavior changed), every
/api/*request from the page returnsHTTP 403— including basic ones likehost.describe,session.list, andllm.discoverModels, not just privileged methods. The UI shows:This happens in loopback mode (
127.0.0.1:<port>) too, not only in LAN mode. Firefox/Edge and curl do not reproduce it, which makes it easy to dismiss as an environment problem.Root cause
Chrome 150 changed Origin serialization for loopback URLs: for a page served from
http://127.0.0.1:3080, same-origin fetch requests now send the Origin header without the port:The trust check in
dsh-client-connection/lib/index.jscompares the Origin against the Host including the port:Because
new URL("http://127.0.0.1").hostis"127.0.0.1"buthostUrl.hostis"127.0.0.1:3080", the comparison returnsfalseand all /api requests are rejected. curl reproduces nothing because it sends the Origin with the port manually.Suggested fix
Compare hostnames only, ignoring the port. The port is not a CSRF / DNS-rebinding boundary: a cross-site attacker's page has a different hostname, and DNS rebinding changes the Host-header hostname; the first gate (Host must be loopback or trusted) plus the
sec-fetch-site: cross-siterejection already cover both. Dropping the port from the comparison does not weaken security.Suggested scope of the change (all 4 rejection points currently use the same
isTrustedApiRequesthelper, so a single fix covers them):fetchHandlerroute checkregisterinterceptor checkAdditional suggestion (LAN mode)
While touching this file, consider also honoring the LAN addresses computed by
dsh-web-app(webRuntime.lanAddresses) whentrustedHostsis empty and the server binds0.0.0.0. Todaydsh-client-connectionpins privileged methods to loopback with an emptytrustedHostslist, so LAN mode serves the page but every /api call is 403 — the two plugins' trust semantics disagree. Details in the same file aroundapply().Verification of the fix
After switching to hostname comparison:
host.describe/session.list/llm.discoverModelsreturn 200 in Chrome 150+; a forged cross-site Origin (evil.example.com) is still rejected (CSRF preserved); a forged LAN IP is still rejected.Thanks for the great project — this was the only blocker for a "double-click to run" green distribution I maintain.
This report was drafted with AI assistance (analysis of the root cause and the fix suggestion were verified against the published npm tarball).
All reactions