siyuan-note/siyuan · error
path escapes templates dir
Error message
path escapes templates dir: %s
What it means
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.
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
Example fix
// before
renderTemplate("../../conf/conf.json")
// after
renderTemplate("meeting-notes.md") // resolves to <data>/templates/meeting-notes.md Defensive patterns
Strategy: validation
Validate before calling
const base = path.join(DATA_DIR, 'templates');
const abs = path.resolve(base, p);
if (!abs.startsWith(base + path.sep)) {
throw new Error(`path escapes templates dir: ${p}`);
} Type guard
function isInsideTemplatesDir(p, dataDir) {
const abs = path.resolve(dataDir, 'templates', p);
const rel = path.relative(path.resolve(dataDir, 'templates'), abs);
return rel !== '' && !rel.startsWith('..') && !path.isAbsolute(rel);
} Try / catch
try {
return await callMcpTool('templateRender', { path: name });
} catch (e) {
if (String(e.message).startsWith('path escapes templates dir')) {
// sanitize the name and retry with the bare filename
}
} Prevention
- 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
When it happens
Trigger: templateRemove/templateRender called with a relative path containing ../ (or an absolute path outside data/templates), e.g. "../../conf/conf.json" or "/etc/passwd".
Common situations: 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.
Understand the failure class
Background: Path traversal blocked: "path escapes the workspace" and "outside site root" errors when a path will not stay inside its allowed directory — this error's family across 26 libraries.
Related errors
- access to sensitive workspace file is forbidden
- child template [ ] is not a regular file
- symlink escapes workspace
- archive entry escapes destination
- asset path is sensitive
AI-assisted analysis of siyuan-note/siyuan@9f775e8a12 (2026-09-19).
Data as JSON: /api/errors/45e01a7163b32b4e.
Report an issue: GitHub.
Appendix: source
Thrown at kernel/mcp/tools/template.go:101
sb.WriteString(fmt.Sprintf("- %s\n", r.Content))
sb.WriteString(fmt.Sprintf(" path: %s\n", r.Path))
}
return CallToolResult{Content: []ContentItem{{Type: "text", Text: sb.String()}}}, nil
}
func resolveTemplatePath(p string) (string, error) {
if p == "" {
return "", fmt.Errorf("path is required")
}
abs := p
if !filepath.IsAbs(abs) {
abs = filepath.Join(util.DataDir, "templates", p)
}
abs = filepath.Clean(abs)
templatesBase := filepath.Clean(filepath.Join(util.DataDir, "templates"))
rel, err := filepath.Rel(templatesBase, abs)
if err != nil || strings.HasPrefix(rel, "..") || rel == ".." {
return "", fmt.Errorf("path escapes templates dir: %s", p)
}
return abs, nil
}
func templateGet(args map[string]any) (CallToolResult, error) {
p, _ := args["path"].(string)
data, err := model.ReadTemplateFile(p)
if err != nil {
return CallToolResult{Content: []ContentItem{{Type: "text", Text: "read template failed: " + err.Error()}}, IsError: true}, nil
}
return CallToolResult{Content: []ContentItem{{Type: "text", Text: string(data)}}}, nil
}
func templateRemove(args map[string]any) (CallToolResult, error) {
p, _ := args["path"].(string)
abs, err := resolveTemplatePath(p)
if err != nil {
return CallToolResult{Content: []ContentItem{{Type: "text", Text: err.Error()}}, IsError: true}, nilView on GitHub (pinned to 9f775e8a12)