GitHub Advisoryadvisory · Go

GHSA-x8gv-g2g3-65fj SiYuan Agent Tools SSRF via DNS-Rebinding TOCTOU (Bypass of CheckHostSSRF)

HIGH Published 2026-10-02 Modified 2026-10-02 Official page ↗
Ecosystem
Go
Packages
github.com/siyuan-note/siyuan/kernel
Severity
High
CVSS vector
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:L/A:N
Weaknesses
CWE-918
Reviewed
2026-10-02

Affected packages

• Go github.com/siyuan-note/siyuan/kernel: < 0.0.0-20260813142806-dd2778b70d02 → fixed in 0.0.0-20260813142806-dd2778b70d02

Description

Security Advisory — SiYuan Agent Tools SSRF via DNS-Rebinding TOCTOU (Bypass of `CheckHostSSRF`) | Field | Value | |---|---| | Disclosed by | joysinleung (`[email protected]`) | | Report date | 2026-08-13 | | Product | SiYuan (思源笔记) — `siyuan-note/siyuan` | | Go module | `github.com/siyuan-note/siyuan/kernel` | | Affected versions | `/secret")` and `util.WebFetch(...)` returned the internal-only proof body, while `CheckHostSSRF("127.0.0.1")` directly blocked and the legit public domain `pub.local` passed — confirming the guard passed but the connection hit the internal address. All versions ` 203.0.113.1 (public, passes CheckHostSSRF) even query (dial) -> 127.0.0.1 (loopback internal victim) Directly invoke the real agent-tool functions (v3.8.0 code path): util.HTTPRequest("GET", "http://rebind.local:15353/secret", ...) util.WebFetch("http://rebind.local:15353/secret", ...) Result (evidence): body = "F9_REBIND_PROOF=reached-internal-only-service-via-TOCTOU" Negative control: CheckHostSSRF("127.0.0.1") -> blocked. Positive control: CheckHostSSRF("pub.local") -> 203.0.113.1, allowed. ``` The loopback victim is a faithful stand-in for `169.254.169.254` / any internal address: the guard allowed a public IP while the connection reached a private one. Impact Confidentiality breach via SSRF: an attacker who can steer the agent can read cloud instance metadata (IAM temporary credentials), internal service responses, and anything reachable from the SiYuan kernel's network position. Scope is changed (`S:C`) because the kernel often runs with cloud-instance privileges. Exploitable under default config (SafeMode on or off). Scope Reachable whenever the agent `http_request` / `web_fetch` tool is usable (default AI tooling). Independent of Publish/auth configuration. Not gated by `SafeMode`. Remediation 1. Connect-time enforcement (preferred): mount `SSRFSafeDialer` (or a dedicated always-on private-IP `Control` hook) on the transport used by `http_request` / `web_fetch`, independent of `SafeMode` — matching the already-correct `generatedImageDialer`. 2. Pin resolution: after `CheckHostSSRF` passes, reuse the *same* resolved IP for the connection (or short-TTL cache) so guard and dial cannot diverge. 3. Minimum change: replace `httpclient.NewBrowserRequest()` in `http_request.go` / `webfetch.go` with a custom client whose `DialContext` is `util.SSRFSafeDialer(timeout).DialContext` (not `SafeMode`-gated). > Note: patch authored against v3.8.0 source; regression-tested in the researcher's environment for the PoC path but not compiled into a full SiYuan release build. Provided for the maintainer to validate in CI. --- Appendix: Suggested Patch (F9) ```diff diff --git a/kernel/util/httprequest.go b/kernel/util/httprequest.go index aaa..bbb 100644 --- a/kernel/util/httprequest.go +++ b/kernel/util/httprequest.go @@ -40,6 +40,18 @@ func CheckHostSSRF(host string) error { return nil } +// SSRFSafeClient returns an *http.Client whose transport enforces the +// private/loopback/link-local IP block at CONNECT time (independent of SafeMode), +// closing the DNS-rebinding TOCTOU left by parse-time-only CheckHostSSRF. +func SSRFSafeClient(timeout time.Duration) *http.Client { + return &http.Client{ + Timeout: timeout, + Transport: &http.Transport{ + DialContext: util.S…

Related

Weakness · CWE (1)

  • CWE-918 Server-Side Request Forgery weakness

References