{"record":{"id":"ae76dc3273e0c6b1","repo":"siyuan-note/siyuan","slug":"invalid-child-template-path","errorCode":null,"errorMessage":"invalid child template path","messagePattern":"invalid child template path","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/model/template_doc_tree.go","lineNumber":402,"sourceCode":"\t\treturn true\n\t})\n\tif maxTemplateDocTreePlans <= count && nil != oldest {\n\t\ttemplateDocTreePlans.Delete(oldest.id)\n\t}\n\ttemplateDocTreePlans.Store(id, plan)\n\ttemplateDocTreePlansLock.Unlock()\n\ttime.AfterFunc(templateDocTreePlanTTL, func() {\n\t\ttemplateDocTreePlans.Delete(id)\n\t})\n\treturn collector.summary(id)\n}\n\nfunc resolveTemplatePackageFile(rootTemplatePath, relativePath string) (string, error) {\n\trelativePath = strings.TrimPrefix(filepath.ToSlash(strings.TrimSpace(relativePath)), \"/\")\n\tcleanPath := filepath.Clean(filepath.FromSlash(relativePath))\n\tif \"\" == cleanPath || \".\" == cleanPath || filepath.IsAbs(cleanPath) || \"..\" == cleanPath ||\n\t\tstrings.HasPrefix(cleanPath, \"..\"+string(os.PathSeparator)) {\n\t\treturn \"\", errors.New(\"invalid child template path\")\n\t}\n\ttemplatesRoot := filepath.Clean(filepath.Join(util.DataDir, \"templates\"))\n\trelRootTemplate, err := filepath.Rel(templatesRoot, filepath.Clean(rootTemplatePath))\n\tif nil != err || strings.HasPrefix(relRootTemplate, \"..\"+string(os.PathSeparator)) {\n\t\treturn \"\", errors.New(\"template path is outside templates directory\")\n\t}\n\tparts := strings.Split(filepath.ToSlash(relRootTemplate), \"/\")\n\tpackageRoot := templatesRoot\n\tif 1 < len(parts) {\n\t\tpackageRoot = filepath.Join(templatesRoot, parts[0])\n\t}\n\tabsPath := filepath.Join(packageRoot, cleanPath)\n\tif !gulu.File.IsSubPath(packageRoot, absPath) || !filelock.IsExist(absPath) {\n\t\treturn \"\", fmt.Errorf(\"child template [%s] not found in the current template package\", relativePath)\n\t}\n\trealRoot, err := filepath.EvalSymlinks(packageRoot)\n\tif nil != err {\n\t\treturn \"\", err","sourceCodeStart":384,"sourceCodeEnd":420,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/8641553a1f07374001902d3ce773285db1292b2d/kernel/model/template_doc_tree.go#L384-L420","documentation":"resolveTemplatePackageFile validates the relative child-template path supplied inside a template package before joining it to disk. The path is rejected when it is empty, '.', absolute, '..', or escapes upward via a '../' prefix. This prevents child templates from referencing files outside the intended resolution root.","triggerScenarios":"Calling renderTemplateDocTreeNodeContent with a template that references a child template via a relative path that is empty, absolute (e.g. '/etc/passwd' style), '..', or starts with '../'. Also triggered directly by the test TestResolveTemplateDocTreeTemplatePathRejectsEscape with crafted escape paths.","commonSituations":"A template author writes {{.child \"../other-template\"}} to reach a sibling package; an empty path results from an unrendered template variable; a Windows-authored template uses an absolute path like 'C:\\templates\\x.md'; a variable used for the child path resolves to blank.","solutions":["Fix the child template reference in the template file to a clean relative path within the same template package (no leading '/', no '..')","Ensure any template variable used in the child path renders to a non-empty relative path","Replace absolute or backslash paths in the template with package-relative forward-slash paths","If the child template lives in another package, move or copy it into the current package instead of escaping upward"],"exampleFix":"// before (in template content)\n{{childTemplate \"../shared/header.tpl\"}}\n// after\n{{childTemplate \"shared/header.tpl\"}}","handlingStrategy":"validation","validationCode":"// Go: pre-validate child template references the same way the kernel does\np := strings.TrimPrefix(filepath.ToSlash(strings.TrimSpace(ref)), \"/\")\nclean := filepath.Clean(filepath.FromSlash(p))\ninvalid := clean == \"\" || clean == \".\" || clean == \"..\" ||\n    filepath.IsAbs(clean) || strings.HasPrefix(clean, \"..\"+string(os.PathSeparator))","typeGuard":null,"tryCatchPattern":"path, err := resolveTemplatePackageFile(root, rel)\nif err != nil {\n    if strings.Contains(err.Error(), \"invalid child template path\") {\n        return fmt.Errorf(\"template references an illegal child path %q\", rel)\n    }\n    return err\n}","preventionTips":["Always write child template references as package-relative forward-slash paths","Never start a child reference with '/' or '..'","Guard template variables used in paths so they cannot render empty","Lint template content for absolute or upward-traversing references before distribution"],"tags":["template","path-validation","path-traversal","siyuan-kernel"],"backgroundTag":"path-traversal-blocked","analyzedSha":"8641553a1f07374001902d3ce773285db1292b2d","analyzedAt":"2026-09-11T16:08:28.414Z","contentChangedAt":"2026-09-11T16:08:28.414Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}