{"record":{"id":"45e01a7163b32b4e","repo":"siyuan-note/siyuan","slug":"path-escapes-templates-dir-s-45e01a","errorCode":null,"errorMessage":"path escapes templates dir: %s","messagePattern":"path escapes templates dir: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/mcp/tools/template.go","lineNumber":101,"sourceCode":"\t\tsb.WriteString(fmt.Sprintf(\"- %s\\n\", r.Content))\n\t\tsb.WriteString(fmt.Sprintf(\"  path: %s\\n\", r.Path))\n\t}\n\treturn CallToolResult{Content: []ContentItem{{Type: \"text\", Text: sb.String()}}}, nil\n}\n\nfunc resolveTemplatePath(p string) (string, error) {\n\tif p == \"\" {\n\t\treturn \"\", fmt.Errorf(\"path is required\")\n\t}\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\ttemplatesBase := filepath.Clean(filepath.Join(util.DataDir, \"templates\"))\n\trel, err := filepath.Rel(templatesBase, abs)\n\tif err != nil || strings.HasPrefix(rel, \"..\") || rel == \"..\" {\n\t\treturn \"\", fmt.Errorf(\"path escapes templates dir: %s\", p)\n\t}\n\treturn abs, nil\n}\n\nfunc templateGet(args map[string]any) (CallToolResult, error) {\n\tp, _ := args[\"path\"].(string)\n\tdata, err := model.ReadTemplateFile(p)\n\tif err != nil {\n\t\treturn CallToolResult{Content: []ContentItem{{Type: \"text\", Text: \"read template failed: \" + err.Error()}}, IsError: true}, nil\n\t}\n\treturn CallToolResult{Content: []ContentItem{{Type: \"text\", Text: string(data)}}}, nil\n}\n\nfunc templateRemove(args map[string]any) (CallToolResult, error) {\n\tp, _ := args[\"path\"].(string)\n\tabs, err := resolveTemplatePath(p)\n\tif err != nil {\n\t\treturn CallToolResult{Content: []ContentItem{{Type: \"text\", Text: err.Error()}}, IsError: true}, nil","sourceCodeStart":83,"sourceCodeEnd":119,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/9f775e8a12daef8255556097396f9b2739078892/kernel/mcp/tools/template.go#L83-L119","documentation":"resolveTemplatePath confines template access to data/templates. After cleaning the path it computes filepath.Rel against the templates base and rejects anything outside (relative result starting with \"..\") to prevent template removal or rendering of arbitrary workspace or system files.","triggerScenarios":"templateRemove/templateRender called with a relative path containing ../ (or an absolute path outside data/templates), e.g. \"../../conf/conf.json\" or \"/etc/passwd\".","commonSituations":"Scripts building template paths by concatenation; attempts to reuse the template tool as a generic file reader; a stored template name that references a parent directory; moving templates to a custom directory and passing the custom absolute path.","solutions":["Pass only the template's name relative to data/templates; let the tool build the absolute path","Remove ../ sequences from user-supplied template names before calling","If templates live elsewhere, move/symlink-check them into data/templates or use the appropriate file tool within its guards"],"exampleFix":"// before\nrenderTemplate(\"../../conf/conf.json\")\n// after\nrenderTemplate(\"meeting-notes.md\") // resolves to <data>/templates/meeting-notes.md","handlingStrategy":"validation","validationCode":"const base = path.join(DATA_DIR, 'templates');\nconst abs = path.resolve(base, p);\nif (!abs.startsWith(base + path.sep)) {\n  throw new Error(`path escapes templates dir: ${p}`);\n}","typeGuard":"function isInsideTemplatesDir(p, dataDir) {\n  const abs = path.resolve(dataDir, 'templates', p);\n  const rel = path.relative(path.resolve(dataDir, 'templates'), abs);\n  return rel !== '' && !rel.startsWith('..') && !path.isAbsolute(rel);\n}","tryCatchPattern":"try {\n  return await callMcpTool('templateRender', { path: name });\n} catch (e) {\n  if (String(e.message).startsWith('path escapes templates dir')) {\n    // sanitize the name and retry with the bare filename\n  }\n}","preventionTips":["Always pass template names relative to data/templates, never ../ or absolute paths","Strip path separators and .. from user-supplied template names","Keep templates inside data/templates; do not relocate the directory and pass external paths"],"tags":["security","filesystem","path-validation","template"],"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"}