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

  1. Pass only the template's name relative to data/templates; let the tool build the absolute path
  2. Remove ../ sequences from user-supplied template names before calling
  3. 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

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


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}, nil

View on GitHub (pinned to 9f775e8a12)