{"record":{"id":"dcb4a452baeec3de","repo":"siyuan-note/siyuan","slug":"template-path-is-outside-templates-directory","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.go","lineNumber":97,"sourceCode":"\tbuf.Grow(4096)\n\terr = tpl.Execute(buf, nil)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(Conf.Language(44), err.Error())\n\t}\n\tret = buf.String()\n\treturn\n}\n\n// RemoveTemplate 删除模板文件，路径必须限定在 <data>/templates/ 目录内，防止任意文件被删除\nfunc RemoveTemplate(p string) (err error) {\n\tabs := p\n\tif !filepath.IsAbs(abs) {\n\t\tabs = filepath.Join(util.DataDir, \"templates\", p)\n\t}\n\tabs = filepath.Clean(abs)\n\ttemplatesRoot := filepath.Clean(filepath.Join(util.DataDir, \"templates\"))\n\tif !gulu.File.IsSubPath(templatesRoot, abs) {\n\t\treturn errors.New(\"template path is outside templates directory\")\n\t}\n\terr = filelock.Remove(abs)\n\tif err != nil {\n\t\tlogging.LogErrorf(\"remove template failed: %s\", err)\n\t}\n\treturn\n}\n\n// getTemplateReadmePaths 返回模板包 README 的相对包根路径集合：恒含 README.md，并合并 template.json 的 readme 字段（大小写敏感）。\nfunc getTemplateReadmePaths(templateDir string) map[string]struct{} {\n\tpaths := map[string]struct{}{\"README.md\": {}}\n\tpkg, err := bazaar.ParsePackageJSON(filepath.Join(templateDir, \"template.json\"))\n\tif err != nil {\n\t\treturn paths\n\t}\n\tfor _, v := range pkg.Readme {\n\t\tv = strings.TrimSpace(v)\n\t\tif \"\" != v {","sourceCodeStart":79,"sourceCodeEnd":115,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/251596fc0de2f9528c00c224252fd073a99973f4/kernel/model/template.go#L79-L115","documentation":"Returned by RemoveTemplate (template.go:97) when the resolved absolute path is not a sub-path of <data>/templates/. This is a security guard preventing arbitrary file deletion. The check uses gulu.File.IsSubPath after filepath.Clean, comparing against the canonical templates root.","triggerScenarios":"The API is called with a 'p' argument containing '..' traversal segments that escape the templates directory after cleaning, e.g. '../../etc/important_file', or an absolute path pointing outside <data>/templates/. Triggered by the remove-template HTTP/CLI endpoint when a malformed or malicious path is supplied.","commonSituations":"A plugin or external script calls the template-remove API with a user-supplied path that is not sanitized; a path crafted to test directory traversal; a relative path that, after filepath.Join with util.DataDir/templates, still resolves above the templates root due to leading '..'.","solutions":["Ensure the path passed to RemoveTemplate is a simple relative filename under templates/ with no '..' segments.","Sanitize input: reject paths containing '..' or starting with '/' before calling the API.","If a legitimate template deletion fails, verify the template file actually lives directly under <data>/templates/ and not in a nested data subdirectory."],"exampleFix":"// before — traversal escapes templates root\nRemoveTemplate(\"../../config/conf.json\")\n\n// after — relative name within templates dir\nRemoveTemplate(\"my-template.md\")","handlingStrategy":"validation","validationCode":"import \"strings\"\nimport \"path/filepath\"\n\nfunc safeTemplateName(p string) bool {\n    p = filepath.ToSlash(filepath.Clean(p))\n    if filepath.IsAbs(p) || strings.Contains(p, \"../\") || strings.HasPrefix(p, \"/\") {\n        return false\n    }\n    return true\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Always pass a plain relative filename (e.g. 'tpl.md') to RemoveTemplate, never user-supplied raw paths.","Sanitize any plugin-supplied path by rejecting '..' segments before calling the API.","Treat RemoveTemplate as a destructive op — confirm the resolved path is under <data>/templates/ client-side first."],"tags":["security","path-traversal","template","validation"],"backgroundTag":null,"analyzedSha":"251596fc0de2f9528c00c224252fd073a99973f4","analyzedAt":"2026-08-12T21:18:37.123Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}