SiYuan: Localhost-trust admin bypass on auth-code-gated endpoints, with potential remote reachability via the fixed-port proxy
漏洞描述
**CVE:** This vulnerability corresponds to [CVE-2026-72809](https://nvd.nist.gov/vuln/detail/CVE-2026-72809). ### Summary The kernel's `CheckAuth` grants `RoleAdministrator` to any request whose `RemoteAddr` is loopback (`127.0.0.1`), for a specific set of endpoints, and these localhost bypasses sit **outside** the `accessAuthCode` gate so they apply even when an access auth code is configured. This is demonstrated live (Part A below). Separately, the fixed-port reverse proxy (`fixedport.go`) forwards requests to the kernel over loopback and injects no authentication token, and no `SetTrustedProxies` is configured, so gin does not derive the client address from forwarding headers. By code inspection, a request forwarded through this proxy would reach the kernel with `RemoteAddr = 127.0.0.1`. **If** the fixed-port proxy is bound to a network interface (via `NetworkServe`) and forwards remote requests to the kernel this way, the localhost bypass would grant a remote unauthenticated caller admin on those endpoints. This second step is established by reading the code but was **not reproduced end-to-end**, and I'm asking the maintainer to confirm the proxy's runtime forwarding behavior (Part B below). ### Details **Localhost-trust admin bypass (proven).** `CheckAuth` (`session.go:298-321`) contains localhost-only bypasses that key off `RemoteAddr` and grant `RoleAdministrator`. They sit outside the `accessAuthCode` gate i.e. they apply even when an access auth code is set and cover `/api/system/exit`, `getNetwork`, `getWorkspaceInfo`, `/assets/*`, and `/export/*`. **Fixed-port proxy behavior (code inspection).** `fixedport.go` is a plain reverse proxy that dials the kernel at `127.0.0.1` and injects no token unlike the publish proxy, which injects a `RoleReader` JWT. There is no `SetTrustedProxies` call, so gin does not rewrite `RemoteAddr` from `X-Forwarded-For`. On this reading, a request forwarded through the fixed-port proxy reaches the kernel with `RemoteAddr = 127.0.0.1`. **The composition (conditional).** If both hold at runtime, then: remote request → fixed-port proxy on a network interface → forwarded to kernel at `127.0.0.1` (no token) → kernel sees `RemoteAddr = 127.0.0.1` → localhost bypass grants admin for the endpoints above. I have proven the final link (localhost → admin) and read the code for the forwarding link, but have not confirmed at runtime that the fixed-port proxy is instantiated and forwards remote requests as loopback in a shipped configuration. **Distinct from the previously-dismissed localhost→admin observation.** That observation concerned the *publish* proxy path, where the injected `RoleReader` JWT causes `CheckAuth` to early-return before reaching the localhost bypasses. The fixed-port proxy injects no token, so a request through it would fall through to the `RemoteAddr`-keyed bypass instead. Different proxy, different code path. ### Proof of Concept **Part A: the localhost bypass grants admin without auth, outside the auth-code gate (demonstrated live).** On a local instance with an access auth code configured, the same no-token `getWorkspaceInfo` request returns different results depending on the source address the kernel sees: - From a non-loopback source (kernel sees a non-127.0.0.1 address): `HTTP 401`. - From `127.0.0.1` (kernel sees loopback): `{"code":0,"data":{"workspaceDir":"/siyuan/workspace",…}}` admin data, no auth, despite `accessAuthCode` being set. This confirms the localhost-trust bypass grants admin for these endpoints and is not gated by the access auth code. **Part B: remote → proxy → loopback (code inspection only; NOT reproduced).** By reading `fixedport.go`, the proxy dials `127.0.0.1`, injects no token, and no `SetTrustedProxies` is set. I was not able to reproduce this end-to-end: the `serve` CLI in the container image tested exposes only `--port` and `--accessAuthCode`, not a flag that instantiates the fixed-port proxy in the `NetworkServe`-on-a-non-default-port shape, so the remote→proxy→loopback chain was not exercised at runtime. I did not invoke the destructive `/api/system/exit` endpoint. **I'm asking the maintainer to confirm: under what conditions is the fixed-port proxy instantiated, does it bind a non-loopback interface under `NetworkServe`, and does it forward to the kernel preserving the client address or as loopback?** That determines whether Part A's bypass is remotely reachable. ### Impact **Confirmed (Part A):** on any deployment where a caller can cause the kernel to see a loopback `RemoteAddr`, the endpoints `/api/system/exit`, `getNetwork`, `getWorkspaceInfo`, `/assets/*`, and `/export/*` are reachable with admin rights without the access auth code i.e. the auth code does not protect these endpoints against a loopback-sourced caller. On its own this is a local/adjacent bypass of the access-code control for those endpoints. If the fixed-port proxy forwards remote requests to the kernel as loopback (to be confirmed by the maintainer), a *remote* unauthenticated attacker obtains those admin capabilities, remote kernel shutdown (DoS), network/workspace-path disclosure, and admin-level asset/export reads that bypass the publish-access filter. This would be a remote authentication bypass. It does not grant the full admin API only the localhost-trusted endpoints. ### Suggested fix Do not derive admin trust from `RemoteAddr` when a proxy forwards over loopback. Options: have the fixed-port proxy inject an explicit role/token (as the publish proxy does) so the kernel authorizes on the claim rather than the source address; or configure `SetTrustedProxies` and derive the real client address before applying any localhost bypass; or require the access auth code for these endpoints regardless of source address. The localhost bypass assumes loopback implies a local trusted caller, any loopback-dialing proxy breaks that assumption. Source Code Location: https://github.com/siyuan-note/siyuan Affected Packages: - go:github.com/siyuan-note/siyuan/kernel, affected < 0.0.0-20260723031701-9c16e9851f0b, patched in 0.0.0-20260723031701-9c16e9851f0b CWEs: - CWE-290: Authentication Bypass by Spoofing CVSS: - Primary: score 8.0, CVSS:3.1/AV:L/AC:L/PR:N/UI:N/S:U/C:H/I:L/A:H - CVSS_V3: score 8.0, CVSS:3.1/AV:L/AC:L/PR:N/UI:N/S:U/C:H/I:L/A:H References: - https://github.com/siyuan-note/siyuan/security/advisories/GHSA-3mp7-4rh5-jrv9 - https://nvd.nist.gov/vuln/detail/CVE-2026-72809 - https://github.com/siyuan-note/siyuan/commit/9c16e9851f0b5d7ed179e5c2fca15a7724666871 - https://www.vulncheck.com/advisories/siyuan-before-authentication-bypass-via-localhost-trust - https://github.com/advisories/GHSA-3mp7-4rh5-jrv9