GitHub Security Advisory: GHSA-2h64-c999-c9r6
Package: github.com/siyuan-note/siyuan/kernel (GO)
Severity: CRITICAL (CVSS 0.0)
CWE: CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting'), CWE-94: Improper Control of Generation of Code ('Code Injection'), CWE-1188: Initialization of a Resource with an Insecure Default
Vulnerable versions: <= 0.0.0[PII_REDACTED]1503-96dfe0bea474
Patched in: unpatched

Summary: SiYuan Affected by Stored XSS via Attribute View Name to Electron Renderer RCE

Details:
## Summary

The kernel stores Attribute View (AV / database) names without any HTML escape, then a render template uses raw `strings.ReplaceAll(tpl, "${avName}", nodeAvName)` to embed the name in HTML before pushing to all clients via WebSocket. Three independent client paths (`render.ts:120` → `outerHTML`, `Title.ts:401` → `innerHTML`, `transaction.ts:559` → `innerHTML`) consume the value without escaping. Because the main BrowserWindow runs `nodeIntegration:true, contextIsolation:false, webSecurity:false` (`app/electron/main.js:407-411`), HTML injection in the renderer becomes Node.js code execution.

Payload is stored on disk under `data/storage/av/<id>.json`, replicates via every sync transport (S3 / WebDAV / cloud), survives `.sy.zip` export-import, and triggers for any role (Administrator / Editor / Reader / publish-service Visitor) opening a doc bound to the AV.

## Details

**Kernel write — no escape.** `kernel/model/attribute_view.go:3244-3255`:
```go
attrView.Name = strings.TrimSpace(operation.Data.(string))
attrView.Name = strings.ReplaceAll(attrView.Name, "\n", " ")
if 512 < utf8.RuneCountInString(attrView.Name) {
    attrView.Name = gulu.Str.SubStr(attrView.Name, 512)
}
err = av.SaveAttributeView(attrView)         // ← no html.EscapeString
```

**Kernel template — raw replace.** `kernel/model/attribute_view.go:3242,3283-3284`:
```go
const attrAvNameTpl = `<span data-av-id="${avID}" ... class="popover__block">${avName}</span>`
// ...
tpl := strings.ReplaceAll(attrAvNameTpl, "${avID}", nodeAvID)
tpl = strings.ReplaceAll(tpl, "${avName}", nodeAvName)   // ← raw
```

**Sink #1 — AV body header → outerHTML.** `app/src/protyle/render/av/render.ts:120` (returned from `genTabHeaderHTML`, written via outerHTML at `render.ts:596`):
```ts
<div contenteditable="${editable}" ... data-title="${data.name || ""}" ...>${data.name || ""}</div>
// ...
e.firstElementChild.outerHTML = `<div class="av__container">${genTabHeaderHTML(...)}...</div>`;
```
Same pattern in `kanban/render.ts:227` and `gallery/render.ts:142`.

**Sink #2 — Doc title attribute strip → innerHTML.** `app/src/protyle/header/Title.ts:396-403`:
```ts
response.data.attrViews.forEach((item: { id: string, name: string }) => {
    avTitle += `<span data-av-id="${item.id}" ... class="popover__block">${item.name}</span>&nbsp;`;
});
nodeAttrHTML += `<div class="protyle-attr--av">...${avTitle}</div>`;
this.element.querySelector(".protyle-attr").innerHTML = nodeAttrHTML;
```

**Sink #3 — WebSocket `updateAttrs` push → innerHTML.** `app/src/protyle/wysiwyg/transaction.ts:549-562,659`:
```ts
const escapeHTML = Lute.EscapeHTMLStr(data.new[key]);
if (key === "bookmark") { bookmarkHTML = `...${escapeHTML}...`; }
else if (key === "name")     { nameHTML  = `...${escapeHTML}...`; }
else if (key === "alias")    { aliasHTML = `...${escapeHTML}...`; }
else if (key === "memo")     { memoHTML  = `...${escapeHTML}...`; }
else if (key === "custom-avs" && data.new["av-names"]) {
    avHTML = `<div class="proty