{"record":{"id":"37f72bf1fd5f46b6","repo":"siyuan-note/siyuan","slug":"symlink-resolves-into-encrypted-notebook-s","errorCode":null,"errorMessage":"symlink resolves into encrypted notebook [%s]","messagePattern":"symlink resolves into encrypted notebook \\[(.+?)\\]","errorType":"http","errorClass":null,"httpStatus":200,"severity":"error","filePath":"kernel/api/archive.go","lineNumber":40,"sourceCode":"\t\"path/filepath\"\n\n\t\"github.com/88250/gulu\"\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/siyuan-note/logging\"\n\t\"github.com/siyuan-note/siyuan/kernel/model\"\n\t\"github.com/siyuan-note/siyuan/kernel/util\"\n)\n\n// rejectEncryptedArchivePath 检查路径是否落入加密笔记本目录（含 symlink 绕过），是则返回错误。\nfunc rejectEncryptedArchivePath(absPath string) error {\n\tboxID := model.ExtractBoxIDFromAssetsPath(absPath)\n\tif boxID != \"\" && model.IsEncryptedBox(boxID) {\n\t\treturn fmt.Errorf(\"path belongs to encrypted notebook [%s]\", boxID)\n\t}\n\tif resolved := util.ResolveLongestExistingParent(absPath); resolved != absPath {\n\t\tboxID = model.ExtractBoxIDFromAssetsPath(resolved)\n\t\tif boxID != \"\" && model.IsEncryptedBox(boxID) {\n\t\t\treturn fmt.Errorf(\"symlink resolves into encrypted notebook [%s]\", boxID)\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc zip(c *gin.Context) {\n\tret := gulu.Ret.NewResult()\n\tdefer c.JSON(http.StatusOK, ret)\n\n\targ, ok := util.JsonArg(c, ret)\n\tif !ok {\n\t\treturn\n\t}\n\n\tvar entryPath, zipFilePath string\n\tif !util.ParseJsonArgs(arg, ret,\n\t\tutil.BindJsonArg(\"path\", &entryPath, true, true),      // 相对于工作空间的路径（待打包目录或文件）\n\t\tutil.BindJsonArg(\"zipPath\", &zipFilePath, true, true), // 相对于工作空间的路径（生成的 zip）","sourceCodeStart":22,"sourceCodeEnd":58,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/afa823b6b4e4f183511e0bc0a3be93caa94c7c97/kernel/api/archive.go#L22-L58","documentation":"The second branch of rejectEncryptedArchivePath (kernel/api/archive.go). After the direct path check passes, the kernel calls util.ResolveLongestExistingParent to resolve symlinks in the longest existing portion of the path. If the resolved location differs from the original and lands inside an encrypted notebook, the archive request is rejected. This closes the symlink bypass where a seemingly innocent path outside the notebook points into it.","triggerScenarios":"A symlink planted in a non-encrypted location (e.g. workspace assets or temp dir) whose target is a directory inside an encrypted notebook, then passing that symlink path to the archive zip API; restored backups or synced folders containing symlinks into the encrypted notebook.","commonSituations":"Users create symlinks to share assets between notebooks and one side is encrypted; sync/copy tooling materializes symlinks into the workspace; a malicious or accidental symlink chain redirects an export path into protected storage.","solutions":["Remove the symlink or repoint it to a non-encrypted location, then retry the archive request","Pass the real target location and use the dedicated, encryption-aware export APIs for encrypted notebooks","Audit the workspace for symlinks resolving into encrypted notebook dirs before running archive jobs"],"exampleFix":null,"handlingStrategy":"validation","validationCode":"// Resolve the longest existing parent of the target path client-side and re-run the\n// encrypted-path check on the resolved value before calling the archive API:\nconst resolved = await resolveLongestExistingParent(targetPath);\nif (isEncryptedNotebookPath(resolved, encryptedBoxIds) || isEncryptedNotebookPath(targetPath, encryptedBoxIds)) {\n  throw new Error('refusing to archive: path resolves into encrypted notebook');\n}","typeGuard":"const resolvesOutsideEncryptedBoxes = async (p: string, encryptedBoxIds: Set<string>): Promise<boolean> => {\n  const real = await fs.realpath(p).catch(() => p);\n  return !isEncryptedNotebookPath(real, encryptedBoxIds);\n};","tryCatchPattern":"if (res.code === -1 && res.msg.startsWith('symlink resolves into encrypted notebook')) {\n  // locate and remove/replace the offending symlink, then retry with the real path\n}","preventionTips":["Run periodic symlink audits over the workspace when encrypted notebooks exist","Never build symlinks that target inside encrypted notebook directories","Resolve symlinks before passing paths to bulk/archive operations"],"tags":["security","encryption","symlink","archive","siyuan"],"backgroundTag":"symlink-escape-detected","analyzedSha":"afa823b6b4e4f183511e0bc0a3be93caa94c7c97","analyzedAt":"2026-08-18T17:04:10.865Z","schemaVersion":2},"datasetVersion":"2026-08-31T19:17:28.585Z"}