{"record":{"id":"da2d4947cd08ccc5","repo":"siyuan-note/siyuan","slug":"template-path-is-outside-templates-directory-da2d49","errorCode":null,"errorMessage":"template path is outside templates directory","messagePattern":"template path is outside templates directory","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/model/template_path.go","lineNumber":28,"sourceCode":"\t\"github.com/siyuan-note/siyuan/kernel/util\"\n)\n\n// openTemplatePath 将绝对或相对路径限制在模板根目录内，文件操作通过根目录句柄防止符号链接越界。\nfunc openTemplatePath(p string) (*os.Root, string, error) {\n\tif p == \"\" {\n\t\treturn nil, \"\", errors.New(\"path is required\")\n\t}\n\tbase, err := filepath.Abs(filepath.Join(util.DataDir, \"templates\"))\n\tif err != nil {\n\t\treturn nil, \"\", err\n\t}\n\tabs := p\n\tif !filepath.IsAbs(abs) {\n\t\tabs = filepath.Join(base, p)\n\t}\n\trel, err := filepath.Rel(base, abs)\n\tif err != nil || rel == \".\" || rel == \"..\" || strings.HasPrefix(rel, \"..\"+string(filepath.Separator)) {\n\t\treturn nil, \"\", errors.New(\"template path is outside templates directory\")\n\t}\n\troot, err := os.OpenRoot(base)\n\treturn root, rel, err\n}\n\n// ReadTemplateFile 在模板根目录内读取普通文件，禁止通过符号链接读取目录外的数据。\nfunc ReadTemplateFile(p string) ([]byte, error) {\n\troot, rel, err := openTemplatePath(p)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer root.Close()\n\tfile, err := root.Open(rel)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer file.Close()\n\tinfo, err := file.Stat()","sourceCodeStart":10,"sourceCodeEnd":46,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/9f775e8a12daef8255556097396f9b2739078892/kernel/model/template_path.go#L10-L46","documentation":"openTemplatePath computes the requested path relative to the templates root and rejects any path that resolves outside it: an empty relative result (the root itself), a parent reference ('..'), or a path beginning with '../'. This enforces the template sandbox so file reads and deletes can never escape into the rest of the workspace or filesystem. Symbolic-link escapes are additionally blocked by performing I/O through an os.Root handle.","triggerScenarios":"Calling ReadTemplateFile or RemoveTemplate with an absolute path outside <DataDir>/templates (e.g. /etc/passwd or another workspace folder), or a relative path containing ../ segments that climb above the templates root; also triggered when the path normalizes exactly to the templates root ('.').","commonSituations":"Storing absolute paths in config that were recorded on another machine or before a workspace move; joining user input like '../../data/foo' into a template path; passing a full filesystem path returned by an older API version that now expects root-relative paths; path traversal in an automated integration.","solutions":["Pass paths relative to the templates directory (e.g. 'sub/dir/template.md'), not absolute filesystem paths","Strip or reject any '..' segments in user-supplied paths before calling","Migrate stored absolute paths by extracting the portion under <DataDir>/templates","If accessing files elsewhere is intended, use the appropriate API — the template helpers are sandboxed by design"],"exampleFix":"// before\ncontent, err := model.ReadTemplateFile(\"/home/user/SiYuan/data/templates/t.md\")\n// after\ncontent, err := model.ReadTemplateFile(\"t.md\")","handlingStrategy":"validation","validationCode":"func safeRel(p string) (string, error) {\n    base := filepath.Join(util.DataDir, \"templates\")\n    abs := p\n    if !filepath.IsAbs(abs) { abs = filepath.Join(base, abs) }\n    rel, err := filepath.Rel(base, abs)\n    if err != nil || rel == \".\" || rel == \"..\" || strings.HasPrefix(rel, \"..\")+string(os.PathSeparator) { return \"\", errors.New(\"outside templates\") }\n    if strings.Contains(rel, \"..\") { return \"\", errors.New(\"outside templates\") }\n    return rel, nil\n}","typeGuard":"func withinTemplates(base, p string) bool { rel, err := filepath.Rel(base, p); return err == nil && rel != \".\" && rel != \"..\" && !strings.HasPrefix(rel, \"..\") }","tryCatchPattern":"if err != nil && strings.Contains(err.Error(), \"outside templates directory\") {\n    // convert the absolute path to a templates-relative path and retry\n}","preventionTips":["Always store and pass templates-relative paths","Reject any user input containing '..' segments","Recompute relative paths after workspace relocation","Never join absolute filesystem paths into template API arguments"],"tags":["security","path-traversal","validation"],"backgroundTag":"path-traversal-blocked","analyzedSha":"9f775e8a12daef8255556097396f9b2739078892","analyzedAt":"2026-09-19T03:17:15.984Z","contentChangedAt":"2026-09-19T03:17:15.984Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}