SiYuan importSY/importZipMd: Path Traversal via multipart filename enables arbitrary file write
Description
SiYuan is a personal knowledge management system. In versions 3.6.0 and below, POST /api/import/importSY and POST /api/import/importZipMd write uploaded archives to a path derived from the multipart filename field without sanitization, allowing an admin to write files to arbitrary locations outside the temp directory - including system paths that enable RCE. This can lead to aata destruction by overwriting workspace or application files, and for Docker containers running as root (common default), this grants full container compromise. This issue has been fixed in version 3.6.1.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
github.com/siyuan-note/siyuan/kernelGo | <= 0.0.0-20260313024916-fd6526133bb3 | — |
Affected products
1- Range: < 3.6.1
Patches
15ee00907f0b0:lock: https://github.com/siyuan-note/siyuan/security/advisories/GHSA-qvvf-q994-x79v
3 files changed · +67 −28
kernel/api/import.go+22 −4 modified@@ -68,8 +68,17 @@ func importSY(c *gin.Context) { ret.Msg = err.Error() return } - writePath := filepath.Join(util.TempDir, "import", file.Filename) + + writePath := filepath.Join(importDir, file.Filename) + if !util.IsSubPath(importDir, writePath) { + logging.LogErrorf("import path [%s] is not sub path of import dir [%s]", writePath, importDir) + ret.Code = -1 + ret.Msg = "import path is not sub path of import dir" + return + } + defer os.RemoveAll(writePath) + writer, err := os.OpenFile(writePath, os.O_RDWR|os.O_CREATE, 0644) if err != nil { logging.LogErrorf("open import .sy.zip [%s] failed: %s", writePath, err) @@ -119,14 +128,14 @@ func importData(c *gin.Context) { return } - tmpImport := filepath.Join(util.TempDir, "import") - err = os.MkdirAll(tmpImport, 0755) + importDir := filepath.Join(util.TempDir, "import") + err = os.MkdirAll(importDir, 0755) if err != nil { ret.Code = -1 ret.Msg = "create temp import dir failed" return } - dataZipPath := filepath.Join(tmpImport, util.CurrentTimeSecondsStr()+".zip") + dataZipPath := filepath.Join(importDir, util.CurrentTimeSecondsStr()+".zip") defer os.RemoveAll(dataZipPath) dataZipFile, err := os.Create(dataZipPath) if err != nil { @@ -225,8 +234,17 @@ func importZipMd(c *gin.Context) { ret.Msg = err.Error() return } + writePath := filepath.Join(util.TempDir, "import", file.Filename) + if !util.IsSubPath(importDir, writePath) { + logging.LogErrorf("import path [%s] is not sub path of import dir [%s]", writePath, importDir) + ret.Code = -1 + ret.Msg = "import path is not sub path of import dir" + return + } + defer os.RemoveAll(writePath) + writer, err := os.OpenFile(writePath, os.O_RDWR|os.O_CREATE, 0644) if err != nil { logging.LogErrorf("open import .zip [%s] failed: %s", writePath, err)
kernel/api/sync.go+30 −16 modified@@ -79,8 +79,15 @@ func importSyncProviderWebDAV(c *gin.Context) { return } - tmp := filepath.Join(importDir, f.Filename) - if err = os.WriteFile(tmp, data, 0644); err != nil { + writePath := filepath.Join(importDir, f.Filename) + if !util.IsSubPath(importDir, writePath) { + logging.LogErrorf("import path [%s] is not sub path of import dir [%s]", writePath, importDir) + ret.Code = -1 + ret.Msg = "import path is not sub path of import dir" + return + } + + if err = os.WriteFile(writePath, data, 0644); err != nil { logging.LogErrorf("import WebDAV provider failed: %s", err) ret.Code = -1 ret.Msg = err.Error() @@ -89,15 +96,15 @@ func importSyncProviderWebDAV(c *gin.Context) { tmpDir := filepath.Join(importDir, "webdav") os.RemoveAll(tmpDir) - if strings.HasSuffix(strings.ToLower(tmp), ".zip") { - if err = gulu.Zip.Unzip(tmp, tmpDir); err != nil { + if strings.HasSuffix(strings.ToLower(writePath), ".zip") { + if err = gulu.Zip.Unzip(writePath, tmpDir); err != nil { logging.LogErrorf("import WebDAV provider failed: %s", err) ret.Code = -1 ret.Msg = err.Error() return } - } else if strings.HasSuffix(strings.ToLower(tmp), ".json") { - if err = gulu.File.CopyFile(tmp, filepath.Join(tmpDir, f.Filename)); err != nil { + } else if strings.HasSuffix(strings.ToLower(writePath), ".json") { + if err = gulu.File.CopyFile(writePath, filepath.Join(tmpDir, f.Filename)); err != nil { logging.LogErrorf("import WebDAV provider failed: %s", err) ret.Code = -1 ret.Msg = err.Error() @@ -124,8 +131,8 @@ func importSyncProviderWebDAV(c *gin.Context) { return } - tmp = filepath.Join(tmpDir, entries[0].Name()) - data, err = os.ReadFile(tmp) + writePath = filepath.Join(tmpDir, entries[0].Name()) + data, err = os.ReadFile(writePath) if err != nil { logging.LogErrorf("import WebDAV provider failed: %s", err) ret.Code = -1 @@ -265,8 +272,15 @@ func importSyncProviderS3(c *gin.Context) { return } - tmp := filepath.Join(importDir, f.Filename) - if err = os.WriteFile(tmp, data, 0644); err != nil { + writePath := filepath.Join(importDir, f.Filename) + if !util.IsSubPath(importDir, writePath) { + logging.LogErrorf("import path [%s] is not sub path of import dir [%s]", writePath, importDir) + ret.Code = -1 + ret.Msg = "import path is not sub path of import dir" + return + } + + if err = os.WriteFile(writePath, data, 0644); err != nil { logging.LogErrorf("import S3 provider failed: %s", err) ret.Code = -1 ret.Msg = err.Error() @@ -275,15 +289,15 @@ func importSyncProviderS3(c *gin.Context) { tmpDir := filepath.Join(importDir, "s3") os.RemoveAll(tmpDir) - if strings.HasSuffix(strings.ToLower(tmp), ".zip") { - if err = gulu.Zip.Unzip(tmp, tmpDir); err != nil { + if strings.HasSuffix(strings.ToLower(writePath), ".zip") { + if err = gulu.Zip.Unzip(writePath, tmpDir); err != nil { logging.LogErrorf("import S3 provider failed: %s", err) ret.Code = -1 ret.Msg = err.Error() return } - } else if strings.HasSuffix(strings.ToLower(tmp), ".json") { - if err = gulu.File.CopyFile(tmp, filepath.Join(tmpDir, f.Filename)); err != nil { + } else if strings.HasSuffix(strings.ToLower(writePath), ".json") { + if err = gulu.File.CopyFile(writePath, filepath.Join(tmpDir, f.Filename)); err != nil { logging.LogErrorf("import S3 provider failed: %s", err) ret.Code = -1 ret.Msg = err.Error() @@ -310,8 +324,8 @@ func importSyncProviderS3(c *gin.Context) { return } - tmp = filepath.Join(tmpDir, entries[0].Name()) - data, err = os.ReadFile(tmp) + writePath = filepath.Join(tmpDir, entries[0].Name()) + data, err = os.ReadFile(writePath) if err != nil { logging.LogErrorf("import S3 provider failed: %s", err) ret.Code = -1
kernel/api/system.go+15 −8 modified@@ -441,8 +441,15 @@ func importConf(c *gin.Context) { return } - tmp := filepath.Join(importDir, f.Filename) - if err = os.WriteFile(tmp, data, 0644); err != nil { + writePath := filepath.Join(importDir, f.Filename) + if !util.IsSubPath(importDir, writePath) { + logging.LogErrorf("import path [%s] is not sub path of import dir [%s]", writePath, importDir) + ret.Code = -1 + ret.Msg = "import path is not sub path of import dir" + return + } + + if err = os.WriteFile(writePath, data, 0644); err != nil { logging.LogErrorf("import conf failed: %s", err) ret.Code = -1 ret.Msg = err.Error() @@ -451,15 +458,15 @@ func importConf(c *gin.Context) { tmpDir := filepath.Join(importDir, "conf") os.RemoveAll(tmpDir) - if strings.HasSuffix(strings.ToLower(tmp), ".zip") { - if err = gulu.Zip.Unzip(tmp, tmpDir); err != nil { + if strings.HasSuffix(strings.ToLower(writePath), ".zip") { + if err = gulu.Zip.Unzip(writePath, tmpDir); err != nil { logging.LogErrorf("import conf failed: %s", err) ret.Code = -1 ret.Msg = err.Error() return } - } else if strings.HasSuffix(strings.ToLower(tmp), ".json") { - if err = gulu.File.CopyFile(tmp, filepath.Join(tmpDir, f.Filename)); err != nil { + } else if strings.HasSuffix(strings.ToLower(writePath), ".json") { + if err = gulu.File.CopyFile(writePath, filepath.Join(tmpDir, f.Filename)); err != nil { logging.LogErrorf("import conf failed: %s", err) ret.Code = -1 ret.Msg = err.Error() @@ -486,8 +493,8 @@ func importConf(c *gin.Context) { return } - tmp = filepath.Join(tmpDir, entries[0].Name()) - data, err = os.ReadFile(tmp) + writePath = filepath.Join(tmpDir, entries[0].Name()) + data, err = os.ReadFile(writePath) if err != nil { logging.LogErrorf("import conf failed: %s", err) ret.Code = -1
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5- github.com/advisories/GHSA-qvvf-q994-x79vghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-32749ghsaADVISORY
- github.com/siyuan-note/siyuan/commit/5ee00907f0b0c4aca748ce21ef1977bb98178e14ghsax_refsource_MISCWEB
- github.com/siyuan-note/siyuan/releases/tag/v3.6.1ghsax_refsource_MISCWEB
- github.com/siyuan-note/siyuan/security/advisories/GHSA-qvvf-q994-x79vghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.