SiYuan's publish/read-only boundary can be broken through /api/av/removeUnusedAttributeView.
A publish-service Reader context can call this endpoint because it is protected only by CheckAuth, and publish requests are forwarded upstream with a valid RoleReader JWT. The handler accepts attacker-controlled id input and passes it directly into a filesystem delete sink:
- no admin check
- no readonly check
- no node-ID validation
- no subpath enforcement
- no verification that the target AV is actually unused
Because the sink builds the file path with:
filepath.Join(util.DataDir, "storage", "av", id+".json")
an attacker can supply ../ path traversal sequences and delete arbitrary .json files reachable from the workspace, rather than only data/storage/av/<id>.json.
This is a real write/destructive authorization bug, not a visible=false listing issue.
Impact
An attacker with publish-reader access can delete arbitrary .json files under the workspace path reachable from data/storage/av/ using traversal payloads.
Examples of realistic targets:
../local -> data/storage/local.json
../../storage/recent-doc -> data/storage/recent-doc.json
../../storage/outline -> data/storage/outline.json
../../storage/criteria -> data/storage/criteria.json
../../../conf/conf -> conf/conf.json
Practical impact includes:
- persistent destruction of Attribute View definitions
- deletion of global workspace configuration
- deletion of local storage and outline state
- corruption of user state that survives reload
- forced reset/recovery flows or broken UI behavior
Even if some files can be regenerated, this is still a publish-reader to persistent server-side delete primitive against workspace data.
Source to Sink
Source 1: publish service issues Reader-role JWTs
kernel/server/proxy/publish.go
The publish reverse proxy forwards requests to the kernel and injects X-Auth-Token.
Source 2: publish accounts are Reader-role accounts
kernel/model/auth.go
Publish accounts are created with RoleReader.
Source 3: generic auth accepts Reader role
kernel/model/session.go
CheckAuth allows:
RoleAdministrator
RoleEditor
RoleReader
Source 4: dangerous route is only guarded by CheckAuth
kernel/api/router.go#L507
ginServer.Handle("POST", "/api/av/removeUnusedAttributeView", model.CheckAuth, removeUnusedAttributeView)
Source 5: attacker-controlled id is passed straight to model
kernel/api/av.go#L32-L45
avID := arg["id"].(string)
model.RemoveUnusedAttributeView(avID)
There is no:
util.InvalidIDPattern(...)
- allowlist
- normalization check
- verification that the caller should modify this object
Sink: path traversal reaches filesystem delete
kernel/model/attribute_view.go#L49-L76
absPath := filepath.Join(util.DataDir, "storage", "av", id+".json")
...
if err = filelock.RemoveWithoutFatal(absPath); err != nil {
Because id is not validated, inputs like ../../../conf/conf resolve outside data/storage/av/.
Examples:
../../../conf/conf -> <workspace>/conf/conf.json
../local -> <workspace>/data/storage/local.json
../../storage/recent-doc -> <workspace>/data/storage/recent-doc.json
../../storage/outline -> <workspace>/data/storage/outline.json
../../storage/criteria -> <workspace>/data/storage/criteria.json
Proof of Concept
Prerequisite:
- publish service enabled
- attacker has publish-reader access, or anonymous publish access if publish auth is disabled
Request:
POST /api/av/removeUnusedAttributeView HTTP/1.1
Host: <publish-host>:6808
Content-Type: application/json
{
"id": "../../../conf/conf"
}
Expected result:
- request is accepted from publish context
- backend resolves target to
<workspace>/conf/conf.json
- target file is removed
Safer validation targets for testing:
{"id":"../local"}
{"id":"../../storage/recent-doc"}
{"id":"../../storage/outline"}
These demonstrate traversal without immediately targeting the main config file.
Root Cause
Two separate authorization/design failures combine here:
- publish Reader contexts are treated as fully authenticated for
CheckAuth routes
- a destructive internal endpoint trusts raw
id input as a filesystem path component
Either issue would be dangerous; together they produce a high-impact delete primitive.
Remediation
1. Lock the endpoint down properly
/api/av/removeUnusedAttributeView should require:
CheckAdminRole
CheckReadonly
2. Validate the identifier
Reject anything that is not a valid AV/node ID. For example, enforce util.InvalidIDPattern(...) or an equivalent strict allowlist.
3. Enforce subpath safety
After path construction, verify the resolved path remains under the intended directory:
base := filepath.Join(util.DataDir, "storage", "av")
absPath := filepath.Join(base, id+".json")
if !util.IsSubPath(base, absPath) {
return error
}
4. Actually verify "unused"
Before deletion, confirm that the requested AV is in the computed unused set. The current single-delete API does not do that at all.
5. Add regression tests
Add negative tests proving a publish-reader context cannot:
- call this endpoint successfully
- delete a valid AV directly
- pass traversal strings such as
../local
- escape
data/storage/av/
Key References
References
SiYuan's publish/read-only boundary can be broken through
/api/av/removeUnusedAttributeView.A publish-service Reader context can call this endpoint because it is protected only by
CheckAuth, and publish requests are forwarded upstream with a validRoleReaderJWT. The handler accepts attacker-controlledidinput and passes it directly into a filesystem delete sink:Because the sink builds the file path with:
an attacker can supply
../path traversal sequences and delete arbitrary.jsonfiles reachable from the workspace, rather than onlydata/storage/av/<id>.json.This is a real write/destructive authorization bug, not a
visible=falselisting issue.Impact
An attacker with publish-reader access can delete arbitrary
.jsonfiles under the workspace path reachable fromdata/storage/av/using traversal payloads.Examples of realistic targets:
../local->data/storage/local.json../../storage/recent-doc->data/storage/recent-doc.json../../storage/outline->data/storage/outline.json../../storage/criteria->data/storage/criteria.json../../../conf/conf->conf/conf.jsonPractical impact includes:
Even if some files can be regenerated, this is still a publish-reader to persistent server-side delete primitive against workspace data.
Source to Sink
Source 1: publish service issues Reader-role JWTs
kernel/server/proxy/publish.goThe publish reverse proxy forwards requests to the kernel and injects
X-Auth-Token.Source 2: publish accounts are Reader-role accounts
kernel/model/auth.goPublish accounts are created with
RoleReader.Source 3: generic auth accepts Reader role
kernel/model/session.goCheckAuthallows:RoleAdministratorRoleEditorRoleReaderSource 4: dangerous route is only guarded by
CheckAuthkernel/api/router.go#L507Source 5: attacker-controlled
idis passed straight to modelkernel/api/av.go#L32-L45There is no:
util.InvalidIDPattern(...)Sink: path traversal reaches filesystem delete
kernel/model/attribute_view.go#L49-L76Because
idis not validated, inputs like../../../conf/confresolve outsidedata/storage/av/.Examples:
Proof of Concept
Prerequisite:
Request:
Expected result:
<workspace>/conf/conf.jsonSafer validation targets for testing:
{"id":"../local"} {"id":"../../storage/recent-doc"} {"id":"../../storage/outline"}These demonstrate traversal without immediately targeting the main config file.
Root Cause
Two separate authorization/design failures combine here:
CheckAuthroutesidinput as a filesystem path componentEither issue would be dangerous; together they produce a high-impact delete primitive.
Remediation
1. Lock the endpoint down properly
/api/av/removeUnusedAttributeViewshould require:CheckAdminRoleCheckReadonly2. Validate the identifier
Reject anything that is not a valid AV/node ID. For example, enforce
util.InvalidIDPattern(...)or an equivalent strict allowlist.3. Enforce subpath safety
After path construction, verify the resolved path remains under the intended directory:
4. Actually verify "unused"
Before deletion, confirm that the requested AV is in the computed unused set. The current single-delete API does not do that at all.
5. Add regression tests
Add negative tests proving a publish-reader context cannot:
../localdata/storage/av/Key References
kernel/api/router.go#L507kernel/api/av.go#L32kernel/model/attribute_view.go#L49kernel/server/proxy/publish.gokernel/model/auth.goCheckAuthacceptingRoleReader:kernel/model/session.go#L201References