VulnWatch VulnWatch
← Back to dashboard
High github · GHSA-x8gv-g2g3-65fj

SiYuan Agent Tools SSRF via DNS-Rebinding TOCTOU (Bypass of CheckHostSSRF)

Published Oct 2, 2026 CVSS 8.2

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 ` 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.SSRFSafeDialer(timeout).DialContext,
+		},
+	}
+}
+
 diff --git a/kernel/mcp/tools/http_request.go b/kernel/mcp/tools/http_request.go
 index ccc..ddd 100644
--- a/kernel/mcp/tools/http_request.go
+++ b/kernel/mcp/tools/http_request.go
@@ -90,7 +90,7 @@ func httpRequest(args map[string]any) (CallToolResult, error) {
 	if serr := util.CheckHostSSRF(u.Hostname()); serr != nil {
 		return CallToolResult{}, serr
 	}
-	resp, err := httpclient.NewBrowserRequest().Get(rawURL)
+	resp, err := util.SSRFSafeClient(30 * time.Second).Get(rawURL)
 	...
 }
 diff --git a/kernel/util/webfetch.go b/kernel/util/webfetch.go
 index eee..fff 100644
--- a/kernel/util/webfetch.go
+++ b/kernel/util/webfetch.go
@@ -50,7 +50,7 @@ func WebFetch(rawURL string, ...) (string, error) {
 	if serr := util.CheckHostSSRF(u.Hostname()); serr != nil {
 		return "", serr
 	}
-	resp, err := httpclient.NewBrowserRequest().Get(rawURL)
+	resp, err := util.SSRFSafeClient(30 * time.Second).Get(rawURL)
 	...
 }

Affected AI Products

ai agent openai
Get the weekly digest. Every Monday: top AI security stories of the week. Free.