GitHub Security Advisory: GHSA-7m5h-w69j-qggg
Package: github.com/siyuan-note/siyuan/kernel (GO)
Severity: HIGH (CVSS 8.1)
CWE: CWE-285: Improper Authorization
Vulnerable versions: < 0.0.0[PII_REDACTED]5653-2f416e5253f1
Patched in: 0.0.0[PII_REDACTED]5653-2f416e5253f1

Summary: SiYuan: Publish Reader Can Arbitrarily Delete Attribute View Files via `/api/av/removeUnusedAttributeView`

Details:
## Summary

An authenticated publish-service reader can invoke `/api/av/removeUnusedAttributeView` and cause persistent deletion of arbitrary attribute view (`AV`) definition files from the workspace.

The route is protected only by generic `CheckAuth`, which accepts publish `RoleReader` requests. The handler forwards a caller-controlled `id` directly into a model function that deletes `data/storage/av/<id>.json` without verifying either:

- that the caller is allowed to perform write/destructive actions; or
- that the target AV is actually unused.

This is a persistent integrity and availability issue reachable from the publish surface.

## Root Cause

### 1. Publish users are issued a `RoleReader` JWT

- [kernel/model/auth.go](/root/audit/siyuan/kernel/model/auth.go#L105)

```go
ClaimsKeyRole: RoleReader,
```

### 2. The publish reverse proxy forwards that token upstream

- [kernel/server/proxy/publish.go](/root/audit/siyuan/kernel/server/proxy/publish.go#L131)
- [kernel/server/proxy/publish.go](/root/audit/siyuan/kernel/server/proxy/publish.go#L186)

### 3. `CheckAuth` accepts `RoleReader`

- [kernel/model/session.go](/root/audit/siyuan/kernel/model/session.go#L201)

```go
if role := GetGinContextRole(c); IsValidRole(role, []Role{
    RoleAdministrator,
    RoleEditor,
    RoleReader,
}) {
    c.Next()
    return
}
```

### 4. The route is exposed with `CheckAuth` only

- [kernel/api/router.go](/root/audit/siyuan/kernel/api/router.go#L507)

```go
ginServer.Handle("POST", "/api/av/removeUnusedAttributeView", model.CheckAuth, removeUnusedAttributeView)
```

There is no `CheckAdminRole` and no `CheckReadonly`.

### 5. The handler forwards attacker-controlled `id` directly to the delete sink

- [kernel/api/av.go](/root/audit/siyuan/kernel/api/av.go#L32)

```go
avID := arg["id"].(string)
model.RemoveUnusedAttributeView(avID)
```

### 6. The model deletes the AV file unconditionally

- [kernel/model/attribute_view.go](/root/audit/siyuan/kernel/model/attribute_view.go#L49)

```go
func RemoveUnusedAttributeView(id string) {
    absPath := filepath.Join(util.DataDir, "storage", "av", id+".json")
    if !filelock.IsExist(absPath) {
        return
    }
    ...
    if err = filelock.RemoveWithoutFatal(absPath); err != nil {
        ...
        return
    }
    IncSync()
}
```

Crucially, this function does **not** verify that the supplied AV is actually unused. The name of the function suggests a cleanup helper, but the implementation is really "delete AV file by id if it exists".

## Attack Prerequisites

- Publish service enabled
- Attacker can access the publish service
- If publish auth is enabled, attacker has valid publish-reader credentials
- Attacker knows an `avID`

## Obtaining `avID`

`avID` is not secret. It is exposed extensively in frontend markup as `data-av-id`.

Examples:

- [app/src/protyle/render/av/render.ts](/root/audit/siyuan/app/src/protyle/render/av/render.ts#L117)
- [app/src/protyle/render/av/layout.ts](/root/audit/siyuan/app/src