# GHSA-xp3q-r38w-vgqm: Relationship to GHSA-hgfg-j9pg-43xw That advisory concerned the administrator's live UI layout, and commit `63c7d5474` resolved it by re


- **Severity:** Not scored
- **Exploited in the wild:** Not known
- **Fix available:** Yes
- **Published:** 23 Oct 2026

## What is GHSA-xp3q-r38w-vgqm?

GHSA-xp3q-r38w-vgqm is a vulnerability in github.com/siyuan-note/siyuan/kernel, published on 23 Oct 2026. 

### Official description

### Relationship to GHSA-hgfg-j9pg-43xw

That advisory concerned the administrator's live UI layout, and commit `63c7d5474` resolved it by removing `FilterUILayoutByPublishIgnore` and nulling `Conf.UILayout` for readers. That fix is correct and complete for the live layout.

This report concerns a different endpoint, a different filter function and a different storage backing. `/api/storage/getLocalStorage` reads `data/storage/local.json` rather than `Conf.UILayout`, and its filter `FilterLocalStorageByPublishAccess` was not touched by that commit. The consequence is that the same serialized tab objects the earlier fix withheld continue to be served, from the closed-tab history rather than the live layout.

### Summary

Three routes, `/api/storage/getLocalStorage`, `getLocalStorageVal` and `getLocalStorageVals`, are registered with `CheckAuth` only and are reachable by the publish `RoleReader` token and anonymously when `Publish.Auth.Enable` is `false`. All three call `model.GetLocalStorage()` and then, for read-only roles, `FilterLocalStorageByPublishAccess`.

That filter begins by assigning the administrator's entire storage map to its return value and then removes three specific fields. Everything else in the map is returned unmodified. A single unauthenticated POST with an empty body returns the administrator's recently closed tabs with their document titles and identifiers, their current search keywords and the paths those searches cover, the expanded private paths in their file tree, and per-document identifiers from several other keys.

### Details

**Routes.** `kernel/api/router.go:87-89` on master, the same block on the development branch:

```go
ginServer.Handle("POST", "/api/storage/getLocalStorage",     model.CheckAuth, getLocalStorage)
ginServer.Handle("POST", "/api/storage/getLocalStorageVal",  model.CheckAuth, getLocalStorageVal)
ginServer.Handle("POST", "/api/storage/getLocalStorageVals", model.CheckAuth, getLocalStorageVals)
```

No `CheckAdminRole`, no `CheckReadonly`.

**The filter is an allowlist inverted into a blocklist.** `kernel/model/publish_access.go:911` on master and `:1481` on the development branch, byte-identical on both:

```go
func FilterLocalStorageByPublishAccess(publishAccess PublishAccess, localStorage map[string]any) (ret map[string]any) {
    ret = localStorage            // the entire administrator map
    publishIgnore := GetInvisiblePublishAccess(publishAccess)
    // clears local-searchkeys["keys"]
    // clears local-searchasset["k"] and ["keys"]
    // blanks local-docinfo["id"], only when bt != nil, visible tier only, no password check
    return
}
```

`local.json` holds roughly thirty keys. Three are sanitized. The rest are returned as stored.

**What survives.** Taking the client's own list of keys it round-trips through this endpoint (`app/src/protyle/util/compatibility.ts:563`), the notable survivors are:

- `local-closed-tabs`. Up to `SIZE_UNDO` entries, each produced by `layoutToJSON(item, tabJSON)` at `app/src/layout/Wnd.ts:779`. This is the same serializer that produced the data in the earlier advisory: tab title, `docIcon`, `rootId`, `notebookId`, `blockId`, `mode`, `action`, and for a Search tab the entire config object. Unfiltered.
- `local-searchdata`. The live document-search configuration, including `k` (the search text), `r` (replace text), `hPath`, `idPath` (the paths the search was scoped to) and `types`. Note the filter clears the search *history* array and the *asset*-search keyword, but leaves the document-search keyword itself. The project's own standard is applied to two of the three related fields.
- `local-filespaths`. `IFilesPath[]` of `{notebookId, openPaths[]}`, the expanded private notebook and folder paths in the file-tree dock.
- `local-fileposition`, `local-outline`, `local-zoom`. Keyed by document or block identifier, so each discloses private document identifiers.
- `local-move-path`. `{keys[], k}`, the move-document dialog's path history and last keyword.
- `local-recent-docs`, `local-history`, `local-flashcard`, `local-dailynoteid`, `local-layouts`.

**The asymmetry is visible in the function signature.** In the same file, `kernel/api/storage.go`:

| Handler | Filter | Receives `*gin.Context` |
|---|---|---|
| `getCriteria` | `FilterCriteriaByPublishAccess(c, ...)` | yes |
| `getRecentDocs` | `FilterRecentDocsByPublishAccess(c, ...)` | yes |
| `getLocalStorage` / `Val` / `Vals` | `FilterLocalStorageByPublishAccess(publishAccess, data)` | no |

The two sibling filters take the request context and can therefore call `CheckPublishAuthCookie` to evaluate the password tier. `FilterLocalStorageByPublishAccess` does not receive the context, so it cannot perform that check at all. The visible-only limitation is fixed in the parameter list rather than in the body.

Separately, `local-recent-docs` passes through this endpoint unfiltered, which bypasses the dedicated `FilterRecentDocsByPublishAccess` that guards the same data on `getRecentDocs`.

**Note on a prior assessment.** `getLocalStorage` appears on lists of endpoints considered correctly filtered, on the basis that it calls a `Filter*ByPublishAccess` function. That observation is accurate and this report does not contradict it. The filter is called. What it does is assign the whole map and remove three fields.

**No persistence side effect.** The in-place mutations the filter performs do not corrupt the administrator's stored state, because `GetLocalStorage` re-reads and unmarshals `storage/local.json` on each call (`kernel/model/storage.go:82`), so the map is per-request. This is noted to pre-empt the concern rather than as part of the finding.

### Proof of Concept

Precondition: publish mode enabled (default port 6808), anonymous when `Publish.Auth.Enable` is `false`, otherwise any publish reader account. An administrator who has used the client normally, closing some document tabs and running a search.

```
POST http://127.0.0.1:6808/api/storage/getLocalStorage
{}

→ 200. The returned map includes, among others:
     local-closed-tabs   entries carrying title, docIcon, rootId, notebookId,
                         blockId for recently closed documents, including
                         documents the reader has no publish access to
     local-searchdata    k, r, hPath, idPath for the administrator's current
                         document search
     local-filespaths    notebookId and openPaths for expanded private folders
     local-fileposition  keys are private document identifiers
```

`getLocalStorageVal` returns any single key by name, so a caller who only wants the closed-tab history can request it directly.

For contrast, requesting the same underlying data through the sibling endpoint shows the filtering that is applied elsewhere:

```
POST http://127.0.0.1:6808/api/storage/getRecentDocs
{}

→ 200, filtered by FilterRecentDocsByPublishAccess
```

while `local-recent-docs` inside the storage response is not.

### Impact

An anonymous reader in publish mode, or any publish `RoleReader`, obtains the administrator's working context: the titles and identifiers of documents they recently had open, what they have been searching for and where, which private folders they have expanded, and per-document identifiers across several other keys. Titles and search keywords are author-written text and routinely describe the subject of the documents they refer to.

The document and notebook identifiers disclosed here are also the input required by identifier-to-path and path-to-content resolution, so the value is not limited to the metadata itself.

No document bodies are returned, and nothing is written. Confidentiality only.

### Suggested fix

Invert the filter into an actual allowlist. Return only the keys a publish reader needs to render published content, which is a short list along the lines of `local-images`, `local-emojis`, `local-pdftheme`, `local-fontstyles` and the theme and zoom preferences, and drop everything

## Which versions are affected by GHSA-xp3q-r38w-vgqm?

2 open source packages ship an affected version, including github.com/siyuan-note/siyuan/kernel.


| Package | Ecosystem | Affected | Fixed in |
| --- | --- | --- | --- |
| `github.com/siyuan-note/siyuan/kernel` | Go | All versions | 0.0.0-20260729005258-4daee87d4152 |
| `github.com/siyuan-note/siyuan/kernel` | Go | All versions | 0.0.0-20260729005258-4daee87d4152 |

## How do I fix GHSA-xp3q-r38w-vgqm?

Upgrade github.com/siyuan-note/siyuan/kernel to 0.0.0-20260729005258-4daee87d4152 or later.

## Is GHSA-xp3q-r38w-vgqm being exploited?

There is no known exploitation of GHSA-xp3q-r38w-vgqm in the wild and no public exploit code.

## Fix prompt for AI coding agents

```text
Check this codebase for GHSA-xp3q-r38w-vgqm and fix it if it is affected.

What it is: GHSA-xp3q-r38w-vgqm is a vulnerability in github.com/siyuan-note/siyuan/kernel, published on 23 Oct 2026. 
Exploitation: There is no known exploitation of GHSA-xp3q-r38w-vgqm in the wild and no public exploit code.

Affected packages and fixed versions:
- github.com/siyuan-note/siyuan/kernel (Go): affected all versions; fixed in 0.0.0-20260729005258-4daee87d4152
- github.com/siyuan-note/siyuan/kernel (Go): affected all versions; fixed in 0.0.0-20260729005258-4daee87d4152

Task:
1. Search the dependency manifests and lockfiles (for example package.json, package-lock.json, requirements.txt, poetry.lock, uv.lock, pom.xml, build.gradle, go.mod, Cargo.lock, Gemfile.lock) and any Dockerfiles for the affected packages, including transitive dependencies.
2. If an affected version is in use, upgrade it to the fixed version for its release line with the project's package manager, so the lockfile is regenerated. Prefer the smallest upgrade that fixes it.
3. If there is no fix, or the upgrade would break the build, do not guess: explain the risk and the safest workaround instead.
4. Run the tests and summarise what changed and why.
5. If nothing in the project is affected, say so and change nothing.

Reference: https://insights.sbomify.com/GHSA-xp3q-r38w-vgqm/ (Markdown: https://insights.sbomify.com/GHSA-xp3q-r38w-vgqm/index.md)
```

## Sources

- OSV: https://osv.dev/vulnerability/GHSA-xp3q-r38w-vgqm

---
From sbomify Insights: https://insights.sbomify.com/GHSA-xp3q-r38w-vgqm/
Summaries are generated and may contain mistakes; the official description above is the record.
