Not a duplicate of either. All three share the missing-throttle root cause, but this one lives in an entirely separate code path (kernel/server/proxy/publish.go's RoundTrip, not kernel/model/session.go's CheckAuth), on a separate network listener (Publish Service port, default 6808, vs. the main kernel API port), guarding a separate credential store (Conf.Publish.Auth.Accounts, arbitrarily many named username/password pairs, vs. the single AccessAuthCode or Api.Token). Fixing CheckAuth() does not touch this file at all. Recommend the same remediation pattern across all three: a shared, per-account (not just global) failed-attempt counter and lockout/backoff, crypto/subtle.ConstantTimeCompare for every credential comparison in the codebase, and minimum length/complexity enforcement wherever a secret is set (setAccessAuthCode, setAPIToken, and the Publish Service accounts settings-save path).
Severity: High
Package
gomod github.com/siyuan-note/siyuan/kernel (Go)
Affected versions
<= 3.7.3 (confirmed present in 3.7.3 by source review; architectural, not a recent regression — maintainers should confirm lower bound)
Description
Summary
SiYuan's Publish Service (Conf.Publish) is a distinct feature from the "lock screen" (AccessAuthCode) and API token (Conf.Api.Token) covered by GHSA-w3xh-mmmh-r54v and GHSA-m6w6-p7pc-fpg2. It runs its own reverse-proxy HTTP(S) listener on a separate TCP port (default 6808), entirely independent of the main kernel API port and its CheckAuth() middleware, specifically so an owner can publish notes/notebooks to the internet and optionally gate them behind named "publish viewer" accounts (Conf.Publish.Auth.Accounts, each with its own username/password, intended for sharing with specific named people such as family or colleagues). The Basic Auth credential check that gates this entire service — PublishServiceTransport.RoundTrip() in kernel/server/proxy/publish.go has no connection whatsoever to WrongAuthCount/NeedCaptcha(), any per-account lockout, or any rate limiter. This is a third, structurally separate instance of the same missing-throttle root cause already reported twice for the main kernel port, but in a subsystem whose entire purpose is unauthenticated internet exposure, and whose passwords are realistically the weakest of the three (chosen by non-technical operators for named collaborators, with no length/complexity enforcement).
Details
The Publish Service is started independently of the main kernel gin server:
// kernel/server/proxy/publish.go
func initPublishListener() (err error) {
listener, err = net.Listen("tcp", fmt.Sprintf("%s:%d", Host, model.Conf.Publish.Port))
...
}
It serves traffic through a raw httputil.ReverseProxy with a custom RoundTripper — this code path never touches kernel/model/session.go's CheckAuth(), WrongAuthCount, or NeedCaptcha() at all; it is a completely independent authentication implementation:
func (PublishServiceTransport) RoundTrip(request *http.Request) (response *http.Response, err error) {
if model.Conf.Publish.Auth.Enable {
// Session Auth
sessionIdCookie, cookieErr := request.Cookie(model.SessionIdCookieName)
if cookieErr == nil {
sessionID := sessionIdCookie.Value
if username := model.GetBasicAuthUsernameBySessionID(sessionID); username != "" {
if account := model.GetBasicAuthAccount(username); account != nil {
request.Header.Set(model.XAuthTokenKey, account.Token)
response, err = publishRoundTripper.RoundTrip(request)
return
}
model.DeleteSession(sessionID)
}
}
// Basic Auth
username, password, ok := request.BasicAuth()
account := model.GetBasicAuthAccount(username)
if !ok ||
account == nil ||
account.Username == "" || // 匿名用户
account.Password != password {
return &http.Response{
StatusCode: http.StatusUnauthorized,
...
}, nil
}
// set session cookie
sessionID := model.GetNewSessionID()
cookie := &http.Cookie{Name: model.SessionIdCookieName, Value: sessionID, Path: "/", HttpOnly: true}
model.AddSession(sessionID, username)
request.Header.Set(model.XAuthTokenKey, account.Token)
response, err = publishRoundTripper.RoundTrip(request)
response.Header.Add("Set-Cookie", cookie.String())
return
}
request.Header.Set(model.XAuthTokenKey, model.GetBasicAuthAccount("").Token)
response, err = publishRoundTripper.RoundTrip(request)
return
}
A search of kernel/server/proxy/publish.go and its backing kernel/model/auth.go for any of WrongAuthCount, NeedCaptcha, Captcha, RateLimit, or Throttle returns zero matches. There is no attempt counter, no lockout, no delay of any kind — every failed guess returns instantly with a plain 401, and the next request can be tried immediately, with no upper bound on attempts per second beyond raw network throughput.
The credential comparison account.Password != password is also a plain Go string comparison rather than crypto/subtle.ConstantTimeCompare — the same secondary timing-side-channel pattern (CWE-208) present in the two previously reported issues.
Why this is realistically exploitable
Conf.Publish.Auth.Accounts (kernel/conf/publish.go, BasicAuthAccount{Username, Password, Memo}) stores plain operator-chosen strings with no server-side minimum length or complexity check anywhere in the settings-save path for this struct (consistent with the same gap already flagged for setAccessAuthCode/setAPIToken).
- The intended audience for this feature is explicitly non-technical: sharing a personal knowledge base with named family members/collaborators. This is a materially weaker-password population than the "workspace admin sets a lock-screen PIN" or "developer wires up an API token" populations covered by the other two reports.
- The Publish Service's entire reason for existing is to be reachable from the open internet (unlike the main kernel API, which is more often bound to localhost/LAN). An attacker doesn't need any special network position — this is the service's designed public entry point.
- A successful guess doesn't just leak the published content — it obtains a persistent session cookie (
publish-visitor-session-id) plus the account's signed JWT (X-Auth-Token, role RoleReader), which is then honored by the downstream kernel per kernel/mcp/server.go's own comment acknowledging this JWT can reach kernel tool-call routes if not carefully scoped (否则 Publish 匿名模式注入的 RoleReader JWT 可经此链路越权调用全部工具 — "otherwise the RoleReader JWT injected by Publish anonymous mode could improperly call every tool through this path").
PoC
Same sandbox constraint as the previous two reports (no Go toolchain/proxy.golang.org access), so the control-flow logic was extracted verbatim into a minimal harness and exercised with live HTTP traffic:
=== 300 wrong password guesses against account 'family', tight loop ===
300 guesses sent, hits: 0, no lockout/captcha encountered at any point
=== Correct password ===
status: 200
No 429/lockout/captcha response was ever observed, at any point in the 300-attempt run, exactly mirroring the real code's absence of any throttle reference.
To reproduce against a real build (Go ≥1.26), targeting a workspace with Publish Service enabled and Basic Auth accounts configured:
# Default publish port is 6808
for pw in sunflower123 password1 family2024 letmein ...; do
curl -s -o /dev/null -w "%{http_code} $pw\n" \
-u "family:$pw" "https://<target>:6808/"
done
# Expected if vulnerable: unlimited 401s with no lockout/captcha at any
# volume, then 200 (plus a Set-Cookie) on the correct password.
Impact
Unauthenticated remote brute-force of any Publish Service account's password, on a service whose default deployment model is direct internet exposure. A successful guess grants:
- Full read access to whatever notebooks/documents were published to that account (potentially far more sensitive than a link the owner intended for one or two trusted people), and
- A
RoleReader-scoped session (cookie + JWT) forwarded to the underlying kernel, which per the maintainers' own code comment in kernel/mcp/server.go has previously required care to avoid being escalated into broader tool-call access through the MCP bridge — meaning this isn't necessarily contained to read-only document viewing depending on what else is reachable under RoleReader at the time of testing.
Affected products
| Field |
Value |
| Ecosystem |
Go |
| Package name |
github.com/siyuan-note/siyuan/kernel |
| Affected versions |
<= 3.7.3 |
| Patched versions |
(none yet — leave blank until a fix is released) |
Severity
| Field |
Value |
| Vector string |
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N |
| Score |
High — read-access impact on published content and the associated reader session/JWT; not scored Critical since it doesn't directly hand over full kernel administrator access the way the AccessAuthCode/API-token issues do, but the population of realistically weak passwords here is larger than either of those. |
Credits
- alhamrizvi-cloud — Reporter
Relationship to GHSA-w3xh-mmmh-r54v and GHSA-m6w6-p7pc-fpg2
Not a duplicate of either. All three share the missing-throttle root cause, but this one lives in an entirely separate code path (
kernel/server/proxy/publish.go'sRoundTrip, notkernel/model/session.go'sCheckAuth), on a separate network listener (Publish Service port, default 6808, vs. the main kernel API port), guarding a separate credential store (Conf.Publish.Auth.Accounts, arbitrarily many named username/password pairs, vs. the singleAccessAuthCodeorApi.Token). FixingCheckAuth()does not touch this file at all. Recommend the same remediation pattern across all three: a shared, per-account (not just global) failed-attempt counter and lockout/backoff,crypto/subtle.ConstantTimeComparefor every credential comparison in the codebase, and minimum length/complexity enforcement wherever a secret is set (setAccessAuthCode,setAPIToken, and the Publish Service accounts settings-save path).Severity: High
Package
gomod
github.com/siyuan-note/siyuan/kernel(Go)Affected versions
<= 3.7.3 (confirmed present in 3.7.3 by source review; architectural, not a recent regression — maintainers should confirm lower bound)
Description
Summary
SiYuan's Publish Service (
Conf.Publish) is a distinct feature from the "lock screen" (AccessAuthCode) and API token (Conf.Api.Token) covered by GHSA-w3xh-mmmh-r54v and GHSA-m6w6-p7pc-fpg2. It runs its own reverse-proxy HTTP(S) listener on a separate TCP port (default6808), entirely independent of the main kernel API port and itsCheckAuth()middleware, specifically so an owner can publish notes/notebooks to the internet and optionally gate them behind named "publish viewer" accounts (Conf.Publish.Auth.Accounts, each with its own username/password, intended for sharing with specific named people such as family or colleagues). The Basic Auth credential check that gates this entire service —PublishServiceTransport.RoundTrip()inkernel/server/proxy/publish.gohas no connection whatsoever toWrongAuthCount/NeedCaptcha(), any per-account lockout, or any rate limiter. This is a third, structurally separate instance of the same missing-throttle root cause already reported twice for the main kernel port, but in a subsystem whose entire purpose is unauthenticated internet exposure, and whose passwords are realistically the weakest of the three (chosen by non-technical operators for named collaborators, with no length/complexity enforcement).Details
The Publish Service is started independently of the main kernel gin server:
It serves traffic through a raw
httputil.ReverseProxywith a customRoundTripper— this code path never toucheskernel/model/session.go'sCheckAuth(),WrongAuthCount, orNeedCaptcha()at all; it is a completely independent authentication implementation:A search of
kernel/server/proxy/publish.goand its backingkernel/model/auth.gofor any ofWrongAuthCount,NeedCaptcha,Captcha,RateLimit, orThrottlereturns zero matches. There is no attempt counter, no lockout, no delay of any kind — every failed guess returns instantly with a plain 401, and the next request can be tried immediately, with no upper bound on attempts per second beyond raw network throughput.The credential comparison
account.Password != passwordis also a plain Go string comparison rather thancrypto/subtle.ConstantTimeCompare— the same secondary timing-side-channel pattern (CWE-208) present in the two previously reported issues.Why this is realistically exploitable
Conf.Publish.Auth.Accounts(kernel/conf/publish.go,BasicAuthAccount{Username, Password, Memo}) stores plain operator-chosen strings with no server-side minimum length or complexity check anywhere in the settings-save path for this struct (consistent with the same gap already flagged forsetAccessAuthCode/setAPIToken).publish-visitor-session-id) plus the account's signed JWT (X-Auth-Token, roleRoleReader), which is then honored by the downstream kernel perkernel/mcp/server.go's own comment acknowledging this JWT can reach kernel tool-call routes if not carefully scoped (否则 Publish 匿名模式注入的 RoleReader JWT 可经此链路越权调用全部工具— "otherwise the RoleReader JWT injected by Publish anonymous mode could improperly call every tool through this path").PoC
Same sandbox constraint as the previous two reports (no Go toolchain/
proxy.golang.orgaccess), so the control-flow logic was extracted verbatim into a minimal harness and exercised with live HTTP traffic:No 429/lockout/captcha response was ever observed, at any point in the 300-attempt run, exactly mirroring the real code's absence of any throttle reference.
To reproduce against a real build (Go ≥1.26), targeting a workspace with Publish Service enabled and Basic Auth accounts configured:
Impact
Unauthenticated remote brute-force of any Publish Service account's password, on a service whose default deployment model is direct internet exposure. A successful guess grants:
RoleReader-scoped session (cookie + JWT) forwarded to the underlying kernel, which per the maintainers' own code comment inkernel/mcp/server.gohas previously required care to avoid being escalated into broader tool-call access through the MCP bridge — meaning this isn't necessarily contained to read-only document viewing depending on what else is reachable underRoleReaderat the time of testing.Affected products
github.com/siyuan-note/siyuan/kernel<= 3.7.3Severity
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:NCredits