{"record":{"id":"cac2074119304d3f","repo":"siyuan-note/siyuan","slug":"export-path-is-outside-export-directory-cac207","errorCode":null,"errorMessage":"export path is outside export directory","messagePattern":"export path is outside export directory","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/model/notebook_bundle.go","lineNumber":196,"sourceCode":"\tif err = os.Rename(partialPath, finalPath); nil != err {\n\t\tlogging.LogErrorf(\"publish notebook bundle failed: %s\", err)\n\t\treturn \"\"\n\t}\n\treturn \"/export/\" + url.PathEscape(filepath.Base(finalPath))\n}\n\nfunc exportedFilePath(exportPath string) (ret string, err error) {\n\tencoded, ok := strings.CutPrefix(exportPath, \"/export/\")\n\tif !ok || encoded == \"\" {\n\t\treturn \"\", errors.New(\"invalid export path\")\n\t}\n\tdecoded, err := url.PathUnescape(encoded)\n\tif nil != err {\n\t\treturn \"\", err\n\t}\n\tret = filepath.Join(util.TempDir, \"export\", filepath.FromSlash(decoded))\n\tif !gulu.File.IsSubPath(filepath.Join(util.TempDir, \"export\"), ret) {\n\t\treturn \"\", errors.New(\"export path is outside export directory\")\n\t}\n\treturn\n}\n\n// ImportSYNotebookBundle 导入批量笔记本包。普通 .sy.zip 返回 bundle=false，由原有导入流程继续处理。\nfunc ImportSYNotebookBundle(zipPath string) (boxIDs []string, bundle bool, err error) {\n\tarchive, openErr := zip.OpenReader(zipPath)\n\tif nil != openErr {\n\t\terr = openErr\n\t\treturn\n\t}\n\tvar manifestData []byte\n\trootName := \"\"\n\tmanifestSuffix := \"/\" + syNotebookBundleManifestPath\n\tfor _, file := range archive.File {\n\t\tif !strings.HasSuffix(file.Name, manifestSuffix) {\n\t\t\tcontinue\n\t\t}","sourceCodeStart":178,"sourceCodeEnd":214,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/8641553a1f07374001902d3ce773285db1292b2d/kernel/model/notebook_bundle.go#L178-L214","documentation":"After decoding, exportedFilePath verifies the result stays inside the temp export directory using gulu.File.IsSubPath. If a crafted or malformed path (e.g. '../' segments) escapes that root, it returns 'export path is outside export directory'. This is a path-traversal guard protecting the export bundle reader.","triggerScenarios":"exportPath contains encoded '../' segments (e.g. '/export/..%2F..%2Fsiyuan.db') or otherwise resolves outside util.TempDir/export when joined.","commonSituations":"Importing a third-party .sy.zip bundle whose internal export references contain traversal segments; tampered export manifests; passing URLs built by concatenating untrusted names.","solutions":["Only process bundle files whose export entries are relative names without '..'","Sanitize/reject suspicious encoded segments before calling","Keep the guard intact — treat this error as a sign the bundle is untrusted and skip it"],"exampleFix":"// before\np := \"/export/\" + url.PathEscape(\"../../../etc/passwd\")\n// after\nif strings.Contains(name, \"..\") { return errors.New(\"unsafe entry\") }\np := \"/export/\" + url.PathEscape(name)","handlingStrategy":"validation","validationCode":"func safeEntry(name string) bool { return name != \"\" && !strings.Contains(name, \"..\") && !strings.ContainsAny(name, \"\\\\\\\"\") }","typeGuard":null,"tryCatchPattern":"if _, err := exportedFilePath(p); err != nil { log.Warnf(\"unsafe export path %q: %v\", p, err); return errBundleUntrusted; }","preventionTips":["Reject bundle entries containing '..' before import","Treat traversal errors as untrusted-bundle signals, not retryable failures","Validate third-party .sy.zip contents before processing"],"tags":["export","security","path-traversal"],"backgroundTag":"path-traversal-blocked","analyzedSha":"8641553a1f07374001902d3ce773285db1292b2d","analyzedAt":"2026-09-11T16:08:28.414Z","contentChangedAt":"2026-09-11T16:08:28.414Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}