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

  1. Remove the symlink or repoint it to a non-encrypted location, then retry the archive request
  2. Pass the real target location and use the dedicated, encryption-aware export APIs for encrypted notebooks
  3. 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

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


AI-assisted analysis of siyuan-note/siyuan@afa823b6b4 (2026-08-18). Data as JSON: /api/errors/37f72bf1fd5f46b6. Report an issue: GitHub.