{"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/251596fc0de2f9528c00c224252fd073a99973f4/kernel/filesys/tree.go#L148-L184","documentation":"Returned by `filesys.ValidateBoxRelativePath` when, after lexical `..` filtering, `gulu.File.IsSubPath(boxRoot, resolved)` reports the resolved path is not inside the box directory. This catches paths that lexically look fine but resolve outside the box root (e.g. via absolute paths or filesystem semantics), closing the gap the `..` check does not cover.","triggerScenarios":"A box-relative path that, once joined with `data/<boxID>/`, lands outside `data/<boxID>` — for example an absolute path component or a value exploiting `IsSubPath` semantics on the host OS.","commonSituations":"Cross-platform path handling where `filepath.Join` produces an unexpected result; a client supplying a path crafted to slip past the `..` check; symlinks whose target the lexical check does not resolve.","solutions":["Use canonical, box-relative paths only; avoid absolute paths in box-relative APIs.","Call `filepath.Clean` and verify the result stays under the box root before invoking tree APIs.","Audit the originating client/sync code to ensure paths are generated server-side from IDs, not from raw input."],"exampleFix":"// before\np := \"/\" + untrustedPath // may resolve outside box\n// after\np := filepath.Clean(untrustedPath)\nif rel, err := filepath.Rel(boxRoot, filepath.Join(boxRoot, p)); err != nil || strings.HasPrefix(rel, \"..\") {\n    return errors.New(\"reject\")\n}","handlingStrategy":"validation","validationCode":"// Defense-in-depth: verify IsSubPath yourself before the call:\nresolved := filepath.Join(util.DataDir, boxID, origP)\nboxRoot := filepath.Join(util.DataDir, boxID)\nif !gulu.File.IsSubPath(boxRoot, resolved) {\n    return \"\", fmt.Errorf(\"path [%s] escapes box directory\", origP)\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Avoid absolute paths in box-relative APIs.","Run filepath.Clean and a Rel-based containment check at the trust boundary.","Test path handling on all target OSes; separators and Join differ."],"tags":["security","path-traversal","filesystem","validation"],"backgroundTag":null,"analyzedSha":"251596fc0de2f9528c00c224252fd073a99973f4","analyzedAt":"2026-08-12T21:18:37.123Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}