{"record":{"id":"f83563aadfb981a4","repo":"siyuan-note/siyuan","slug":"path-s-escapes-box-directory","errorCode":null,"errorMessage":"path [%s] escapes box directory","messagePattern":"path \\[(.+?)\\] escapes box directory","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/filesys/tree.go","lineNumber":166,"sourceCode":"// 允许路径以 / 开头（如 /20230101/xxx.sy），会自动标准化再去掉前导斜杠。\n// 根路径（\"/\" 或 \"\"）合法，返回空字符串。\nfunc ValidateBoxRelativePath(boxID, p string) (string, error) {\n\tp = filepath.ToSlash(p)\n\t// 记录原始路径用于 IsSubPath 校验\n\torigP := p\n\t// 标准化：去掉前导 /\n\tp = strings.TrimPrefix(p, \"/\")\n\t// 根路径直接放行（box 根目录本身是合法路径）\n\tif p == \"\" {\n\t\treturn p, nil\n\t}\n\tif strings.HasPrefix(p, \"..\") || strings.Contains(p, \"/../\") || strings.HasSuffix(p, \"/..\") || p == \"..\" || p == \".\" {\n\t\treturn \"\", fmt.Errorf(\"path [%s] must not contain '..'\", origP)\n\t}\n\tresolved := filepath.Join(util.DataDir, boxID, origP)\n\tboxRoot := filepath.Join(util.DataDir, boxID)\n\tif !gulu.File.IsSubPath(boxRoot, resolved) {\n\t\treturn \"\", fmt.Errorf(\"path [%s] escapes box directory\", origP)\n\t}\n\treturn p, nil\n}\n\nfunc LoadTreeWithFix(boxID, p string, luteEngine *lute.Lute) (ret *parse.Tree, needFix bool, err error) {\n\tif _, err = ValidateBoxRelativePath(boxID, p); err != nil {\n\t\tlogging.LogErrorf(\"invalid tree path [%s] for box [%s]: %s\", p, boxID, err)\n\t\treturn\n\t}\n\n\tdek, encrypted, releaseCryptoLease, leaseErr := acquireCryptoLease(boxID)\n\tif leaseErr != nil {\n\t\terr = leaseErr\n\t\treturn\n\t}\n\tdefer releaseCryptoLease()\n\n\trootID := util.GetTreeID(p)","sourceCodeStart":148,"sourceCodeEnd":184,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/9f775e8a12daef8255556097396f9b2739078892/kernel/filesys/tree.go#L148-L184","documentation":"After the textual \"..\" check, ValidateBoxRelativePath resolves the path against util.DataDir/boxID and uses gulu.File.IsSubPath to confirm the absolute result stays inside the notebook root. The error means the path passed the string checks but still resolves outside the box directory (e.g. via symlinks or separator tricks).","triggerScenarios":"Paths that after filepath.Join/Clean escape the box root — symlinked directories inside the notebook pointing outside, boxID itself containing traversal-like content, or platform-specific path forms the string check missed.","commonSituations":"Notebooks containing symlinks to external folders; corrupted box IDs; running the same data directory across OSes with different path semantics.","solutions":["Verify the path resolves inside the notebook's data/<boxID> directory and remove external links.","Check the boxID is a valid notebook ID and the path is relative to that notebook.","Remove or replace symlinks inside the notebook that point outside the workspace."],"exampleFix":"// before\nValidateBoxRelativePath(boxID, \"/link-to-external/doc.sy\") // link points outside the box\n// after\n// place doc.sy inside the notebook and use \"/doc.sy\"","handlingStrategy":"validation","validationCode":"const resolved = require(\"path\").resolve(boxRoot, relPath);\nif (!resolved.startsWith(boxRoot + require(\"path\").sep)) throw new Error(\"resolved path escapes box\");","typeGuard":null,"tryCatchPattern":"if _, err := filesys.ValidateBoxRelativePath(boxID, p); err != nil {\n    log.Warnf(\"path rejected: %v\", err)\n    return\n}","preventionTips":["Avoid symlinks inside notebook data directories that point outside.","Validate box IDs as plain node-ID-like strings.","Run one OS per data directory; do not share workspaces across platforms."],"tags":["validation","path-traversal","security","filesystem"],"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"}