{"record":{"id":"22345e85a394a5fd","repo":"siyuan-note/siyuan","slug":"path-is-not-a-child-of-assets-directory-s","errorCode":null,"errorMessage":"path is not a child of assets directory: %s","messagePattern":"path is not a child of assets directory: (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/model/assets.go","lineNumber":945,"sourceCode":"\t\t\tif !filelock.IsExist(boxConfPath) {\n\t\t\t\terr = fmt.Errorf(\"asset path does not belong to a notebook: %s\", assetPath)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif IsEncryptedBox(parts[0]) {\n\t\t\t\terr = fmt.Errorf(\"accessing assets in encrypted notebook [%s] is not supported\", parts[0])\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}\n\tif assetDirIndex < 0 {\n\t\terr = fmt.Errorf(\"path is not under an assets directory: %s\", assetPath)\n\t\treturn\n\t}\n\n\tassetRootParts := parts[:assetDirIndex+1]\n\tassetRoot := filepath.Join(util.DataDir, filepath.FromSlash(strings.Join(assetRootParts, \"/\")))\n\tif !gulu.File.IsSubPath(assetRoot, absPath) {\n\t\terr = fmt.Errorf(\"path is not a child of assets directory: %s\", assetPath)\n\t\treturn\n\t}\n\n\tresolvedRoot, evalErr := filepath.EvalSymlinks(assetRoot)\n\tif evalErr != nil {\n\t\terr = fmt.Errorf(\"resolve assets directory [%s] failed: %w\", assetRoot, evalErr)\n\t\treturn\n\t}\n\tif assetDirIndex > 0 {\n\t\tnotebookRoot := filepath.Join(util.DataDir, parts[0])\n\t\tresolvedDataDir, dataEvalErr := filepath.EvalSymlinks(util.DataDir)\n\t\tresolvedNotebookRoot, notebookEvalErr := filepath.EvalSymlinks(notebookRoot)\n\t\tif dataEvalErr != nil || notebookEvalErr != nil ||\n\t\t\t!gulu.File.IsSubPath(resolvedDataDir, resolvedNotebookRoot) ||\n\t\t\t!gulu.File.IsSubPath(resolvedNotebookRoot, resolvedRoot) {\n\t\t\terr = fmt.Errorf(\"notebook asset path resolves outside notebook directory: %s\", assetPath)\n\t\t\treturn\n\t\t}","sourceCodeStart":927,"sourceCodeEnd":963,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/251596fc0de2f9528c00c224252fd073a99973f4/kernel/model/assets.go#L927-L963","documentation":"Thrown by ResolveDataAssetPath (kernel/model/assets.go:945) when the computed absolute path of an asset is not located beneath the assets root directory derived from the same input. It is a defense-in-depth invariant check that runs after the input has already been cleaned and confirmed to contain an `assets` path segment, so under normal operation it should never fire; when it does, it means the path and the detected assets root disagree (typically a malformed or adversarial path that survived earlier cleaning).","triggerScenarios":"Calling model.ResolveDataAssetPath (directly or via cli/cmd/asset.go, mcp/tools/asset.go, server/serve.go:919) with a path string that, after filepath.Clean + filepath.Rel against util.DataDir, contains an `assets` segment but whose resulting absPath is not a descendant of the reconstructed assets root. This is essentially unreachable through ordinary file paths because Clean+Rel already normalize traversal.","commonSituations":"Developers almost never hit this in production. If observed, it usually indicates either a programmatic caller constructing a non-standard asset path (e.g. embedding NUL bytes or OS-specific separators that confuse Split vs Join), a bug introduced when refactoring the path logic, or a filesystem where DataDir itself is a symlink that changes the Rel result unexpectedly.","solutions":["Inspect the exact assetPath string passed in (it is included via %s) and re-run it through filepath.Clean + filepath.Rel(util.DataDir, ...) manually to see why the assets-root membership check fails.","Confirm util.DataDir is a real directory and not itself a broken/looping symlink; if it is, fix the workspace bootstrap so DataDir resolves to a stable physical directory.","If you are a caller building the asset path dynamically, build it with filepath.Join on already-validated segments instead of string concatenation, and ensure it is relative to util.DataDir with an `assets/` prefix.","Treat occurrences as a security signal: do not silence it, log the raw input and stack trace, and reject the request."],"exampleFix":null,"handlingStrategy":"validation","validationCode":"// Reject input that cannot possibly be a data-relative asset path before calling ResolveDataAssetPath.\nfunc validDataAssetPath(p string) error {\n    if p == \"\" { return errors.New(\"asset path is required\") }\n    if filepath.IsAbs(p) { return errors.New(\"asset path must be relative\") }\n    cleaned := filepath.Clean(filepath.FromSlash(p))\n    rel, err := filepath.Rel(util.DataDir, filepath.Join(util.DataDir, cleaned))\n    if err != nil || rel == \".\" || rel == \"..\" || strings.HasPrefix(rel, \"..\"+string(filepath.Separator)) {\n        return errors.New(\"asset path escapes data directory\")\n    }\n    return nil\n}","typeGuard":null,"tryCatchPattern":"relativePath, absPath, err := model.ResolveDataAssetPath(assetPath)\nif err != nil {\n    // log assetPath for audit; do not fall back to raw filesystem access\n    logging.LogWarningf(\"resolve data asset failed: %s, err: %s\", assetPath, err)\n    return \"\", err\n}","preventionTips":["Always pass a relative path rooted at util.DataDir with an assets/ or <boxID>/assets/ prefix.","Build asset paths with filepath.Join over validated segments, never string concatenation of user input.","Do not catch this and retry with a transformed path — treat it as a reject signal."],"tags":["assets","path-traversal","security","validation","filesystem"],"backgroundTag":null,"analyzedSha":"251596fc0de2f9528c00c224252fd073a99973f4","analyzedAt":"2026-08-12T21:18:37.123Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}