{"record":{"id":"33cb8adb26bfab30","repo":"siyuan-note/siyuan","slug":"obsidian-vault-path-is-unsafe-vault-root-is-a-sym","errorCode":null,"errorMessage":"Obsidian Vault path is unsafe: Vault root is a symbolic link or reparse point","messagePattern":"Obsidian Vault path is unsafe: Vault root is a symbolic link or reparse point","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/model/import_obsidian.go","lineNumber":574,"sourceCode":"}\n\nfunc validateObsidianVaultRoot(localPath string) (string, error) {\n\tif strings.TrimSpace(localPath) == \"\" {\n\t\treturn \"\", fmt.Errorf(\"%w: path is empty\", errObsidianVaultUnreadable)\n\t}\n\tabs, err := filepath.Abs(filepath.Clean(localPath))\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"%w: normalize Vault path: %v\", errObsidianVaultUnreadable, err)\n\t}\n\tinfo, err := os.Lstat(abs)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"%w: read Vault root: %v\", errObsidianVaultUnreadable, err)\n\t}\n\tif !info.IsDir() {\n\t\treturn \"\", errObsidianVaultNotDirectory\n\t}\n\tif info.Mode()&os.ModeSymlink != 0 || isObsidianResolvedLink(abs) {\n\t\treturn \"\", fmt.Errorf(\"%w: Vault root is a symbolic link or reparse point\", errObsidianVaultUnsafePath)\n\t}\n\tif util.IsSensitivePath(abs) {\n\t\treturn \"\", fmt.Errorf(\"%w: selected Vault path is sensitive\", errObsidianVaultUnsafePath)\n\t}\n\tworkspace, _ := filepath.Abs(filepath.Clean(util.WorkspaceDir))\n\tif sameObsidianPath(abs, workspace) || gulu.File.IsSubPath(workspace, abs) || gulu.File.IsSubPath(abs, workspace) {\n\t\treturn \"\", fmt.Errorf(\"%w: Vault root and SiYuan workspace contain each other\", errObsidianVaultUnsafePath)\n\t}\n\tconfigPath := filepath.Join(abs, \".obsidian\")\n\tconfigInfo, statErr := os.Lstat(configPath)\n\tif statErr != nil {\n\t\tif os.IsNotExist(statErr) {\n\t\t\treturn \"\", errObsidianVaultConfigMissing\n\t\t}\n\t\treturn \"\", fmt.Errorf(\"%w: read Vault config directory: %v\", errObsidianVaultUnreadable, statErr)\n\t}\n\tif !configInfo.IsDir() || configInfo.Mode()&os.ModeSymlink != 0 || isObsidianResolvedLink(configPath) {\n\t\treturn \"\", errObsidianVaultConfigMissing","sourceCodeStart":556,"sourceCodeEnd":592,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/251596fc0de2f9528c00c224252fd073a99973f4/kernel/model/import_obsidian.go#L556-L592","documentation":"A wrapped variant of errObsidianVaultUnsafePath (line 573-574): returned when the Vault root is a symbolic link or a Windows reparse point. The check tests both the Lstat Mode()&os.ModeSymlink flag and isObsidianResolvedLink(abs), which follows links that Lstat alone might miss. The error wraps the unsafe sentinel with ': Vault root is a symbolic link or reparse point'. This prevents symlink-based traversal attacks where a link escapes the intended directory.","triggerScenarios":"POST /api/import/startObsidianVaultAnalysis with a localPath that is a symlink (Unix) or junction/reparse point (Windows), or resolves through isObsidianResolvedLink to a link target. Line 573 fires.","commonSituations":"User organized Vaults via symlinks; a cloud-storage client created a symlinked folder; on macOS, some folder arrangements under /Volumes use firmlinks; the Vault root is a Windows junction.","solutions":["Resolve the symlink to its real target with filepath.EvalSymlinks and pass the physical directory.","Replace the symlink/junction with the actual Vault directory, or copy the Vault contents to a non-linked folder.","Select the real underlying folder in the UI picker rather than the link."],"exampleFix":"// before\nlocalPath := \"/home/user/vault-link\" // symlink\n// -> \"Obsidian Vault path is unsafe: Vault root is a symbolic link or reparse point\"\n\n// after\nreal, _ := filepath.EvalSymlinks(\"/home/user/vault-link\")\nlocalPath := real // e.g. /mnt/data/MyVault","handlingStrategy":"validation","validationCode":"// Resolve symlinks before calling the API\nreal, err := filepath.EvalSymlinks(localPath)\nif err != nil { return err }\ninfo, err := os.Lstat(real)\nif err != nil { return err }\nif info.Mode()&os.ModeSymlink != 0 { return errors.New(\"path is still a symlink after resolution\") }","typeGuard":null,"tryCatchPattern":"if _, err := model.StartObsidianVaultAnalysis(localPath); err != nil {\n    if errors.Is(err, errObsidianVaultUnsafePath) && strings.Contains(err.Error(), \"symbolic link\") {\n        return errors.New(\"the Vault root is a symlink; please select the real folder\")\n    }\n}","preventionTips":["Resolve symlinks/junctions to their physical target before importing.","Avoid organizing Vaults via symlinks; use real directories.","Select the underlying physical folder in the picker."],"tags":["obsidian","import","security","validation","symlink","reparse-point"],"backgroundTag":null,"analyzedSha":"251596fc0de2f9528c00c224252fd073a99973f4","analyzedAt":"2026-08-12T21:18:37.123Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}