GHSA-3MP7-4RH5-JRV9
Vulnerability from github – Published: 2026-09-03 22:34 – Updated: 2026-09-03 22:34CVE: This vulnerability corresponds to 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.
{
"affected": [
{
"package": {
"ecosystem": "Go",
"name": "github.com/siyuan-note/siyuan/kernel"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "0.0.0-20260723031701-9c16e9851f0b"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-72809"
],
"database_specific": {
"cwe_ids": [
"CWE-290"
],
"github_reviewed": true,
"github_reviewed_at": "2026-09-03T22:34:06Z",
"nvd_published_at": null,
"severity": "HIGH"
},
"details": "**CVE:** This vulnerability corresponds to [CVE-2026-72809](https://nvd.nist.gov/vuln/detail/CVE-2026-72809).\n\n### Summary\n\nThe kernel\u0027s `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).\n\nSeparately, 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\u0027m asking the maintainer to confirm the proxy\u0027s runtime forwarding behavior (Part B below).\n\n### Details\n\n**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/*`.\n\n**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`.\n\n**The composition (conditional).** If both hold at runtime, then: remote request \u2192 fixed-port proxy on a network interface \u2192 forwarded to kernel at `127.0.0.1` (no token) \u2192 kernel sees `RemoteAddr = 127.0.0.1` \u2192 localhost bypass grants admin for the endpoints above. I have proven the final link (localhost \u2192 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.\n\n**Distinct from the previously-dismissed localhost\u2192admin 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.\n\n### Proof of Concept\n\n**Part A: the localhost bypass grants admin without auth, outside the auth-code gate (demonstrated live).**\nOn 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:\n- From a non-loopback source (kernel sees a non-127.0.0.1 address): `HTTP 401`.\n- From `127.0.0.1` (kernel sees loopback): `{\"code\":0,\"data\":{\"workspaceDir\":\"/siyuan/workspace\",\u2026}}` admin data, no auth, despite `accessAuthCode` being set.\n\nThis confirms the localhost-trust bypass grants admin for these endpoints and is not gated by the access auth code.\n\n**Part B: remote \u2192 proxy \u2192 loopback (code inspection only; NOT reproduced).**\nBy 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\u2192proxy\u2192loopback chain was not exercised at runtime. I did not invoke the destructive `/api/system/exit` endpoint. **I\u0027m 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\u0027s bypass is remotely reachable.\n\n### Impact\n\n**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.\n\n### Suggested fix\n\nDo 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.",
"id": "GHSA-3mp7-4rh5-jrv9",
"modified": "2026-09-03T22:34:06Z",
"published": "2026-09-03T22:34:06Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/siyuan-note/siyuan/security/advisories/GHSA-3mp7-4rh5-jrv9"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-72809"
},
{
"type": "WEB",
"url": "https://github.com/siyuan-note/siyuan/commit/9c16e9851f0b5d7ed179e5c2fca15a7724666871"
},
{
"type": "PACKAGE",
"url": "https://github.com/siyuan-note/siyuan"
},
{
"type": "WEB",
"url": "https://www.vulncheck.com/advisories/siyuan-before-authentication-bypass-via-localhost-trust"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:L/AC:L/PR:N/UI:N/S:U/C:H/I:L/A:H",
"type": "CVSS_V3"
}
],
"summary": "SiYuan: Localhost-trust admin bypass on auth-code-gated endpoints, with potential remote reachability via the fixed-port proxy"
}
Sightings
| Author | Source | Type | Date | Other |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.
The approach is described in our paper Mapping CVEs to MITRE ATT&CK Techniques: A Curated Gold-Set Classifier and the Limits of LLM-Assisted Label Expansion.