{"record":{"id":"1a5765286515aa8c","repo":"siyuan-note/siyuan","slug":"notebook-asset-path-resolves-outside-notebook-dire","errorCode":null,"errorMessage":"notebook asset path resolves outside notebook directory: %s","messagePattern":"notebook asset path resolves outside notebook directory: (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/model/assets.go","lineNumber":961,"sourceCode":"\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}\n\t}\n\tresolvedPath, evalErr := filepath.EvalSymlinks(absPath)\n\tif evalErr != nil {\n\t\terr = fmt.Errorf(\"resolve asset [%s] failed: %w\", absPath, evalErr)\n\t\treturn\n\t}\n\tif !gulu.File.IsSubPath(resolvedRoot, resolvedPath) {\n\t\terr = fmt.Errorf(\"asset path resolves outside assets directory: %s\", assetPath)\n\t\treturn\n\t}\n\n\trelativePath = filepath.ToSlash(dataRelativePath)\n\treturn\n}\n\n// ResolveUnusedDataAssetPath 解析 data 相对资源路径，并确认目标当前未被引用。","sourceCodeStart":943,"sourceCodeEnd":979,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/251596fc0de2f9528c00c224252fd073a99973f4/kernel/model/assets.go#L943-L979","documentation":"Thrown by ResolveDataAssetPath (kernel/model/assets.go:961) only for notebook-relative asset paths (assetDirIndex > 0). After resolving symlinks, it verifies that the resolved notebook root still sits inside the resolved DataDir AND that the resolved assets root sits inside the resolved notebook root. If either chain is broken — typically because a symlink inside the notebook escapes the notebook — the request is rejected. This is a path-traversal / symlink-escape security guard, exercised by TestResolveDataAssetPath.","triggerScenarios":"Calling ResolveDataAssetPath with a path like `<boxID>/linked/assets/file.png` where `<boxID>/linked` is a symlink to a directory outside `<boxID>/`, so the resolved assets root no longer lives under the notebook root. Also fires if the notebook directory itself is a symlink that resolves outside DataDir.","commonSituations":"A user (or a maliciously crafted document) places a symlink inside a notebook that points elsewhere on disk to expose or overwrite files; a workspace that was restructured with symlinks for compatibility; migrating a notebook by symlinking its old location that lives outside the new DataDir.","solutions":["Audit symlinks inside the offending notebook: `find <DataDir>/<boxID> -type l -ls` and identify which link resolves outside the notebook.","Replace the escaping symlink with either a real copy of the asset or a relative symlink that stays inside the notebook directory.","If this is unexpected, treat it as a security event: the input may come from an untrusted document, so reject the document/asset rather than weakening the check.","Do not attempt to bypass by editing the guard; instead store the asset under the notebook's own assets/ directory."],"exampleFix":null,"handlingStrategy":"validation","validationCode":"// Reject asset paths whose notebook subpath traverses a symlink out of the notebook.\nfunc notebookSelfContained(boxDir, assetRel string) bool {\n    full := filepath.Join(boxDir, filepath.FromSlash(assetRel))\n    real, err := filepath.EvalSymlinks(full)\n    if err != nil { return false }\n    realBox, err := filepath.EvalSymlinks(boxDir)\n    if err != nil { return false }\n    return gulu.File.IsSubPath(realBox, real)\n}","typeGuard":null,"tryCatchPattern":"if _, _, err := model.ResolveDataAssetPath(p); err != nil && strings.Contains(err.Error(), \"resolves outside notebook\") {\n    // a symlink inside the notebook escapes the notebook; do not attempt to follow it manually\n    logging.LogWarningf(\"rejected escaping notebook symlink for asset: %s\", p)\n}","preventionTips":["Never symlink notebook subdirectories to locations outside the notebook.","When importing assets, copy files physically into <boxID>/assets/ rather than linking.","Periodically audit `find <DataDir>/<boxID> -type l` for links whose targets leave the notebook."],"tags":["assets","symlinks","path-traversal","security","notebooks"],"backgroundTag":null,"analyzedSha":"251596fc0de2f9528c00c224252fd073a99973f4","analyzedAt":"2026-08-12T21:18:37.123Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}