{"record":{"id":"a94aabe9a0663313","repo":"siyuan-note/siyuan","slug":"template-directory-contains-a-symbolic-link","errorCode":null,"errorMessage":"template directory contains a symbolic link","messagePattern":"template directory contains a symbolic link","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/model/template_manage.go","lineNumber":134,"sourceCode":"}\n\nfunc templateFileRevision(root *os.Root, p string) (string, error) {\n\tinfo, err := root.Stat(p)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\th := sha256.New()\n\tif info.IsDir() {\n\t\terr = fs.WalkDir(root.FS(), p, func(name string, entry fs.DirEntry, walkErr error) error {\n\t\t\tif walkErr != nil {\n\t\t\t\treturn walkErr\n\t\t\t}\n\t\t\tstat, statErr := entry.Info()\n\t\t\tif statErr != nil {\n\t\t\t\treturn statErr\n\t\t\t}\n\t\t\tif stat.Mode()&os.ModeSymlink != 0 {\n\t\t\t\treturn errors.New(\"template directory contains a symbolic link\")\n\t\t\t}\n\t\t\tfmt.Fprintf(h, \"%s\\x00%d\\x00%d\\x00%d\\n\", name, stat.Size(), stat.ModTime().UnixNano(), stat.Mode())\n\t\t\treturn nil\n\t\t})\n\t} else {\n\t\tif info.Size() > maxTemplateSourceSize {\n\t\t\treturn \"\", errors.New(\"template source is too large\")\n\t\t}\n\t\tvar content []byte\n\t\tcontent, err = root.ReadFile(p)\n\t\th.Write(content)\n\t}\n\treturn fmt.Sprintf(\"%x\", h.Sum(nil)), err\n}\n\nfunc readTemplateSource(root *os.Root, p string) (string, error) {\n\tinfo, err := root.Stat(p)\n\tif err != nil {","sourceCodeStart":116,"sourceCodeEnd":152,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/8641553a1f07374001902d3ce773285db1292b2d/kernel/model/template_manage.go#L116-L152","documentation":"While hashing a template directory (computing a manifest over its entries), a directory entry was found to be a symbolic link. Template directories must contain only regular entries; symlinks are refused so the manifest/hash reflects only real contained data and cannot be used to reach outside the directory.","triggerScenarios":"The template-directory hash/manifest walk calls entry.Info() on each child and stat.Mode()&os.ModeSymlink != 0, e.g. a symlinked file or subdirectory placed inside a managed template package directory.","commonSituations":"Symlinks inside shared/synced template folders (e.g. created on macOS/Linux then surfaced elsewhere); maliciously planted symlinks; build tooling leaving link artifacts in the template directory.","solutions":["Remove or replace the symlink inside the template directory with a real file/directory","Rescan after cleaning; the hash/manifest will then succeed","Keep symlinks out of directories managed as template packages"],"exampleFix":"// before\n$ ln -s ../shared/theme.css templates/pkg/theme.css\n// after\n$ rm templates/pkg/theme.css\n$ cp ../shared/theme.css templates/pkg/theme.css\n","handlingStrategy":"validation","validationCode":"func dirHasSymlinks(dir string) (bool, error) {\n    found := false\n    err := filepath.WalkDir(dir, func(_ string, d fs.DirEntry, err error) error {\n        if err != nil { return err }\n        if d.Type()&fs.ModeSymlink != 0 { found = true }\n        return nil\n    })\n    return found, err\n}\n// refuse to use the directory if dirHasSymlinks returns true","typeGuard":null,"tryCatchPattern":"if err := hashTemplateDir(root, p, h); err != nil {\n    return fmt.Errorf(\"template dir must be symlink-free: %w\", err)\n}","preventionTips":["Keep managed template directories free of symlinks","Copy real files instead of linking when assembling template packages","Re-check directories after sync tools or archives may have introduced links"],"tags":["template","filesystem","symlink","directory"],"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-14T05:17:10.506Z"}