{"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":1093,"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 := ResolveAssetPathWithMissingLeaf(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 := ResolveRealPath(util.DataDir)\n\t\tresolvedNotebookRoot, notebookEvalErr := ResolveRealPath(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 := ResolveAssetPathWithMissingLeaf(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":1075,"sourceCodeEnd":1111,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/9f775e8a12daef8255556097396f9b2739078892/kernel/model/assets.go#L1075-L1111","documentation":"For notebook-level assets (assetDirIndex > 0), ResolveDataAssetPath evaluates the real paths of the data dir, the notebook root, and the assets root, then requires that the notebook root is inside the data dir and the assets root is inside the notebook root. This catches symlinks that redirect a notebook's assets directory outside its own notebook — a path-traversal / containment attack or an unintended link. When containment fails (or any real-path resolution errors), this error is thrown.","triggerScenarios":"Calling ResolveDataAssetPath with `<notebookID>/assets/...` where `assets` (or the notebook dir itself) is a symlink pointing outside the notebook directory, or where the data dir / notebook root real-path resolution fails (deleted or broken path).","commonSituations":"Users symlinking a notebook's assets folder to another location (external drive, shared folder); sync tools replacing directories with links; deliberate path-traversal attempts against the API; workspace directories renamed while stale symlinks remain.","solutions":["Replace the symlink with a real directory inside the notebook and copy the assets back","Point the path at the actual location of the assets instead of through the symlink","Verify the notebook directory itself exists and resolves inside the workspace data dir","If containment was set up intentionally (shared assets), disable that setup — the resolver intentionally forbids it for security"],"exampleFix":"// before (assets is a symlink to /mnt/shared/assets)\nln -s /mnt/shared/assets data/20240101120000-abc123/assets\n// after (real directory inside the notebook)\nmv /mnt/shared/assets/* data/20240101120000-abc123/assets/\nrm data/20240101120000-abc123/assets-link && mkdir data/20240101120000-abc123/assets","handlingStrategy":"try-catch","validationCode":"func notebookAssetsContained(dataDir, notebookID string) bool {\n    eval := func(p string) (string, error) { return filepath.EvalSymlinks(p) }\n    root, err1 := eval(filepath.Join(dataDir, notebookID, \"assets\"))\n    nb, err2 := eval(filepath.Join(dataDir, notebookID))\n    dd, err3 := eval(dataDir)\n    if err1 != nil || err2 != nil || err3 != nil {\n        return false\n    }\n    return strings.HasPrefix(root, nb+string(os.PathSeparator)) && strings.HasPrefix(nb, dd+string(os.PathSeparator))\n}","typeGuard":null,"tryCatchPattern":"rel, abs, err := model.ResolveDataAssetPath(assetPath)\nif err != nil {\n    if strings.Contains(err.Error(), \"resolves outside notebook directory\") {\n        return fmt.Errorf(\"refusing asset %q: symlink escapes notebook (remove the link)\", assetPath)\n    }\n    return err\n}","preventionTips":["Never symlink a notebook's assets directory outside the notebook","Avoid sync/backup tools that replace directories with links","Run the containment precheck (EvalSymlinks + prefix check) before batch asset operations","Keep the notebook directory itself a real directory inside the workspace data dir"],"tags":["go","security","symlink","path-traversal"],"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"}