siyuan-note/siyuan · error

template path is outside templates directory

Error message

template path is outside templates directory

What it means

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.

Solutions

  1. Pass paths relative to the templates directory (e.g. 'sub/dir/template.md'), not absolute filesystem paths
  2. Strip or reject any '..' segments in user-supplied paths before calling
  3. Migrate stored absolute paths by extracting the portion under <DataDir>/templates
  4. If accessing files elsewhere is intended, use the appropriate API — the template helpers are sandboxed by design

Example fix

// before
content, err := model.ReadTemplateFile("/home/user/SiYuan/data/templates/t.md")
// after
content, err := model.ReadTemplateFile("t.md")
Defensive patterns

Strategy: validation

Validate before calling

func safeRel(p string) (string, error) {
    base := filepath.Join(util.DataDir, "templates")
    abs := p
    if !filepath.IsAbs(abs) { abs = filepath.Join(base, abs) }
    rel, err := filepath.Rel(base, abs)
    if err != nil || rel == "." || rel == ".." || strings.HasPrefix(rel, "..")+string(os.PathSeparator) { return "", errors.New("outside templates") }
    if strings.Contains(rel, "..") { return "", errors.New("outside templates") }
    return rel, nil
}

Type guard

func withinTemplates(base, p string) bool { rel, err := filepath.Rel(base, p); return err == nil && rel != "." && rel != ".." && !strings.HasPrefix(rel, "..") }

Try / catch

if err != nil && strings.Contains(err.Error(), "outside templates directory") {
    // convert the absolute path to a templates-relative path and retry
}

Prevention

When it happens

Trigger: 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 ('.').

Common situations: 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.

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/da2d4947cd08ccc5. Report an issue: GitHub.

Appendix: source

Thrown at kernel/model/template_path.go:28

	"github.com/siyuan-note/siyuan/kernel/util"
)

// openTemplatePath 将绝对或相对路径限制在模板根目录内,文件操作通过根目录句柄防止符号链接越界。
func openTemplatePath(p string) (*os.Root, string, error) {
	if p == "" {
		return nil, "", errors.New("path is required")
	}
	base, err := filepath.Abs(filepath.Join(util.DataDir, "templates"))
	if err != nil {
		return nil, "", err
	}
	abs := p
	if !filepath.IsAbs(abs) {
		abs = filepath.Join(base, p)
	}
	rel, err := filepath.Rel(base, abs)
	if err != nil || rel == "." || rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
		return nil, "", errors.New("template path is outside templates directory")
	}
	root, err := os.OpenRoot(base)
	return root, rel, err
}

// ReadTemplateFile 在模板根目录内读取普通文件,禁止通过符号链接读取目录外的数据。
func ReadTemplateFile(p string) ([]byte, error) {
	root, rel, err := openTemplatePath(p)
	if err != nil {
		return nil, err
	}
	defer root.Close()
	file, err := root.Open(rel)
	if err != nil {
		return nil, err
	}
	defer file.Close()
	info, err := file.Stat()

View on GitHub (pinned to 9f775e8a12)