siyuan-note/siyuan · error
symlink resolves into encrypted notebook [%s]
Error message
symlink resolves into encrypted notebook [%s]
What it means
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.
Source
Thrown at kernel/api/archive.go:40
"path/filepath"
"github.com/88250/gulu"
"github.com/gin-gonic/gin"
"github.com/siyuan-note/logging"
"github.com/siyuan-note/siyuan/kernel/model"
"github.com/siyuan-note/siyuan/kernel/util"
)
// rejectEncryptedArchivePath 检查路径是否落入加密笔记本目录(含 symlink 绕过),是则返回错误。
func rejectEncryptedArchivePath(absPath string) error {
boxID := model.ExtractBoxIDFromAssetsPath(absPath)
if boxID != "" && model.IsEncryptedBox(boxID) {
return fmt.Errorf("path belongs to encrypted notebook [%s]", boxID)
}
if resolved := util.ResolveLongestExistingParent(absPath); resolved != absPath {
boxID = model.ExtractBoxIDFromAssetsPath(resolved)
if boxID != "" && model.IsEncryptedBox(boxID) {
return fmt.Errorf("symlink resolves into encrypted notebook [%s]", boxID)
}
}
return nil
}
func zip(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
var entryPath, zipFilePath string
if !util.ParseJsonArgs(arg, ret,
util.BindJsonArg("path", &entryPath, true, true), // 相对于工作空间的路径(待打包目录或文件)
util.BindJsonArg("zipPath", &zipFilePath, true, true), // 相对于工作空间的路径(生成的 zip)View on GitHub (pinned to afa823b6b4)
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
Defensive patterns
Strategy: validation
Validate before calling
// Resolve the longest existing parent of the target path client-side and re-run the
// encrypted-path check on the resolved value before calling the archive API:
const resolved = await resolveLongestExistingParent(targetPath);
if (isEncryptedNotebookPath(resolved, encryptedBoxIds) || isEncryptedNotebookPath(targetPath, encryptedBoxIds)) {
throw new Error('refusing to archive: path resolves into encrypted notebook');
} Type guard
const resolvesOutsideEncryptedBoxes = async (p: string, encryptedBoxIds: Set<string>): Promise<boolean> => {
const real = await fs.realpath(p).catch(() => p);
return !isEncryptedNotebookPath(real, encryptedBoxIds);
}; Try / catch
if (res.code === -1 && res.msg.startsWith('symlink resolves into encrypted notebook')) {
// locate and remove/replace the offending symlink, then retry with the real path
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- path belongs to encrypted notebook [%s]
- encrypted notebook is locked, please unlock it first
- refuse to write decrypted asset inside workspace
- source is not an encrypted asset
- invalid agent runtime turn state
AI-assisted analysis of siyuan-note/siyuan@afa823b6b4 (2026-08-18).
Data as JSON: /api/errors/37f72bf1fd5f46b6.
Report an issue: GitHub.