logo

Database

Server-side request forgery (SSRF) In github.com/siyuan-note/siyuan/kernel

Description

SiYuan: Cross-Site WebSocket Hijacking on the admin-only network proxy endpoint (/ws/network/proxy) via explicit CheckOrigin: true bypass High

Package

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

Affected versions

3.7.3

Patched versions

(none yet — leave blank until a fix is released)

Description

Summary

/ws/network/proxy is an admin-only WebSocket forward-proxy endpoint (target URL and headers fully attacker-specifiable via query parameters). Its websocket.Upgrader explicitly overrides CheckOrigin to unconditionally return true — disabling the origin validation that the gorilla/websocket library otherwise enforces by default. WebSocket handshake requests are not subject to CORS preflight at all (unlike fetch/XHR), so origin validation for WebSocket endpoints has to be done deliberately by the server; here it has been deliberately turned off instead. Combined with the endpoint's own query-parameter-driven proxy target, this is a textbook Cross-Site WebSocket Hijacking (CSWSH) primitive on a capability that amounts to an authenticated network pivot through the SiYuan kernel process.

Details

// kernel/api/network.go:501
upgrader := websocket.Upgrader{
    CheckOrigin: func(r *http.Request) bool { return true },
}
clientConn, upgradeErr := upgrader.Upgrade(c.Writer, c.Request, upgradeHeaders)

Route registration (admin-role-gated):

// kernel/api/router.go:614
ginServer.Handle("GET", "/ws/network/proxy", model.CheckAuth, model.CheckAdminRole, wsProxy)

The proxy target is fully attacker-controllable via query parameters, decoded and dialed directly:

// kernel/api/network.go:348
func parseForwardProxyParams(c *gin.Context) (parsedURL *url.URL, headers *http.Header, timeout time.Duration, err error) {
    uParam := c.Query("u")
    ...
    uBytes, decErr := base64.RawURLEncoding.DecodeString(uParam)
    ...
    parsedURL, err = url.ParseRequestURI(string(uBytes))
    ......

A malicious webpage can construct, entirely from JavaScript with no special access:

new WebSocket("ws://127.0.0.1:6806/ws/network/proxy?u=" + base64url(attackerChosenTargetURL));

WebSocket handshake requests are GET requests carrying ambient cookies exactly like any other cross-site navigation, and are not covered by CORS preflight protections at all, this is a distinct attack surface from ordinary fetch/XHR-based CSRF, and easy to overlook precisely because the usual CORS mental model doesn't apply to it. Whether this is currently exploitable in a given browser depends on the same session-cookie SameSite configuration already covered by a separate report on this repository (no explicit SameSite is set on the session cookie), but even where that provides incidental protection today, the explicit CheckOrigin: func(r *http.Request) bool { return true } override removes a defense-in-depth layer that would otherwise exist automatically from the WebSocket library's own safe default, and is worth fixing independently of the cookie-attribute question.

Impact

If reachable (dependent on browser/cookie-attribute behavior at time of exploitation, as above), a malicious website visited by a user with an active, admin-privileged SiYuan session could open a WebSocket connection to this endpoint and direct the SiYuan kernel process to proxy arbitrary network traffic to an attacker-chosen target, effectively an authenticated SSRF/network-pivot primitive, using the victim's own machine and any network position it has (e.g., internal/localhost-only services on the victim's LAN that aren't reachable from the public internet), entirely via a drive-by visit to an unrelated website while SiYuan happens to be running.

PoC

No live browser PoC was run for this report, this is a code-level confirmation that the CheckOrigin override exists and unconditionally returns true, combined with tracing the fully attacker-controlled proxy-target construction. I also checked whether the other WebSocket-adjacent endpoints (/ws/plugin/rpc, /ws/broadcast) share this issue: they use a different WebSocket library (gws, not gorilla/websocket) with a different upgrade code path I have not independently verified for its own origin-checking defaults, flagging this as worth a follow-up check by your team rather than claiming it applies there too.

## Affected products

| Field | Value |
|---|---|
| Ecosystem | **Go** |
| Package name | `github.com/siyuan-note/siyuan/kernel` |
| Affected versions | `<= 3.7.3` (confirmed present in 3.7.3; maintainers should confirm lower bound) |
| Patched versions | *(none yet — leave blank until a fix is released)* |...

Suggested Fix

Replace CheckOrigin: func(r *http.Request) bool { return true } with a real check — validate the Origin header against the expected local/loopback origin (or the configured workspace's own address), mirroring how IsLoopbackCallback-style validation is already done correctly elsewhere in this codebase (e.g. the MCP OAuth client's loopback-callback check). Also worth auditing the gws-based WebSocket endpoints (/ws/plugin/rpc, /ws/broadcast) for their own origin-validation defaults, since I did not verify those independently.

Mitigation

Update Impact

Minimal update. May introduce new vulnerabilities or breaking changes.

Ecosystem
Component
Affected version
Patched versions