GHSA-x8gv-g2g3-65fj

Suggest an improvement
Source
https://github.com/advisories/GHSA-x8gv-g2g3-65fj
Import Source
https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/10/GHSA-x8gv-g2g3-65fj/GHSA-x8gv-g2g3-65fj.json
JSON Data
https://api.osv.dev/v1/vulns/GHSA-x8gv-g2g3-65fj
Aliases
Published
2026-10-02T23:17:16Z
Modified
2026-10-02T23:30:05Z
Severity
  • 8.2 (High) CVSS_V3 - CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:L/A:N CVSS Calculator
Summary
SiYuan Agent Tools SSRF via DNS-Rebinding TOCTOU (Bypass of CheckHostSSRF)
Details

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

Field Value
Disclosed by joysinleung (joysinleung@gmail.com)
Report date 2026-08-13
Product SiYuan (思源笔记) — siyuan-note/siyuan
Go module github.com/siyuan-note/siyuan/kernel
Affected versions <= 3.8.0 (latest release at report time; dynamically verified on v3.8.0)
Patched versions 3.8.1
Component kernel/util/httprequest.go (CheckHostSSRF), kernel/mcp/tools/http_request.go, kernel/util/webfetch.go, kernel/util/net.go (SSRFSafeDialer)
Relationship to prior advisory Incomplete-fix variant of GHSA-rg26-cg95-gq6p (SSRF main-vector remediation). See §Relationship.
EPSS (exploitation probability) Low–Moderate. Requires the attacker to influence an AI Agent / MCP client into fetching an attacker-controlled domain (prompt-injection scenario documented by the tool itself).
KEV (CISA Known Exploited) No (not listed in CISA KEV at report time).
Default-config reachable Yes — exploitable under both SafeMode on and off; only requires the agent http_request / web_fetch tool to be reachable (default AI tooling).

Summary

SiYuan's AI Agent tools http_request (util.HTTPRequest) and web_fetch (util.WebFetch) are the only SSRF gate for outbound requests from the kernel. That gate is CheckHostSSRF, which performs a single DNS resolution at guard time and checks whether any returned IP is private/loopback/link-local. The actual connection, however, performs a second, independent DNS resolution through the default net.Dialer — and no connect-time private-IP check is mounted on this path.

Because the two resolutions are not pinned to the same result, an attacker-controlled domain can answer the guard-resolution with a public IP (passing CheckHostSSRF) and the connect-resolution with a private/loopback/metadata IP (e.g. 169.254.169.254). This is a classic DNS-rebinding TOCTOU that bypasses the SSRF defense entirely. It reaches cloud instance metadata and internal services that the guard was specifically added to block.

Relationship to Prior Advisories

  • GHSA-rg26-cg95-gq6p remediated the SSRF main vector by adding CheckHostSSRF (parse-time) and SSRFSafeDialer (connect-time). However, the agent tool paths (http_request / web_fetch) only received the parse-time half: they call CheckHostSSRF but then connect via httpclient.NewBrowserRequest(), whose transport does not mount SSRFSafeDialer. Even where SSRFSafeDialer is mounted, it only blocks private IPs when SafeMode == true (default false), so it would not help here regardless. The sibling path openai.go:generatedImageDialer does mount a connect-time Control hook (blocking private/loopback/link-local/100.64/198.18), proving the project knows the technique — the agent path is a clear omission. We report this as an incomplete-fix variant with a concrete v3.8.0 reproduction.
  • CVE-2026-32110 (GHSA-56cv-c5p2-j2wg) covered the older forwardProxy endpoint and is unrelated to the agent tool path.

Affected Version

Dynamically verified on v3.8.0 (tag v3.8.0, commit 251596fc0). A process-level DNS hijack was installed so that the attacker domain rebind.local returns a public IP (203.0.113.1) on odd (guard) resolutions and 127.0.0.1 on even (connect) resolutions; a loopback "victim" service returned F9_REBIND_PROOF=reached-internal-only-service-via-TOCTOU. Both util.HTTPRequest("GET", "http://rebind.local:<port>/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 <= 3.8.0 are affected.

Component

  • kernel/util/httprequest.go:42 CheckHostSSRF — single net.LookupIP + isPrivateIP at guard time only.
  • kernel/mcp/tools/http_request.go:90 → util.HTTPRequest; kernel/util/webfetch.go:50 → CheckHostSSRF + httpclient.NewBrowserRequest().
  • github.com/siyuan-note/httpclient client.go:92 NewBrowserRequest uses the default http.Transport → default net.Dialer with no SSRFSafeDialer.
  • kernel/util/net.go:151 SSRFSafeDialer exists but is (a) not mounted on the agent path and (b) only active under SafeMode.
  • Correctly-defended sibling: kernel/util/openai.go:829 generatedImageDialer mounts a connect-time Control hook.

Attack Vector

Network + AI Agent. The url of http_request / web_fetch is fully controlled by the agent / MCP client. In SiYuan's documented red-team scenario ("prompt-inject the agent → induce it to visit an attacker domain"), the attacker needs only a rebinding domain (own authoritative DNS, first answer public, later 169.254.169.254 / internal). No auth, no special position beyond prompting the agent. Real targets: cloud metadata 169.254.169.254 (IMDSv1 IAM creds) and same-host/internal unauthenticated services.

Proof of Concept

# Attacker authoritative DNS for rebind.local:
#   odd  query (guard)  -> 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 --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)
 	...
 }
Database specific
{
    "cwe_ids":  [
        "CWE-918"
    ],
    "github_reviewed":  true,
    "github_reviewed_at":  "2026-10-02T23:17:16Z",
    "nvd_published_at":  null,
    "severity":  "HIGH"
}
References

Affected packages

Go / github.com/siyuan-note/siyuan/kernel

Package

Name
github.com/siyuan-note/siyuan/kernel
View open source insights on deps.dev
Purl
pkg:golang/github.com/siyuan-note/siyuan/kernel

Affected ranges

Type
SEMVER
Events
Introduced
0 Unknown introduced version / All previous versions are affected
Fixed
0.0.0-20260813142806-dd2778b70d02

Database specific

source
"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/10/GHSA-x8gv-g2g3-65fj/GHSA-x8gv-g2g3-65fj.json"