GitHub Advisoryadvisory · Go
GHSA-x8gv-g2g3-65fj SiYuan Agent Tools SSRF via DNS-Rebinding TOCTOU (Bypass of CheckHostSSRF)
HIGH
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