{"record":{"id":"b359aac2fcc28f04","repo":"siyuan-note/siyuan","slug":"asset-path-resolves-outside-notebook-assets-direct","errorCode":null,"errorMessage":"asset path resolves outside notebook assets directory","messagePattern":"asset path resolves outside notebook assets directory","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/model/asset_download_read.go","lineNumber":112,"sourceCode":"\t\t\treturn \"\", errors.New(\"asset path escapes data directory\")\n\t\t}\n\t\tif boxID == \"\" && !IsEncryptedAssetPath(absPath) {\n\t\t\tif _, _, resolveErr := ResolveDataAssetPath(lookupPath); resolveErr != nil {\n\t\t\t\treturn \"\", resolveErr\n\t\t\t}\n\t\t} else {\n\t\t\tresolvedBoxID := boxID\n\t\t\tif resolvedBoxID == \"\" {\n\t\t\t\tresolvedBoxID = ExtractBoxIDFromAssetsPath(absPath)\n\t\t\t}\n\t\t\troot, rootErr := ResolveAssetPathWithMissingLeaf(filepath.Join(util.DataDir, resolvedBoxID, \"assets\"))\n\t\t\tresolved, resolveErr := ResolveAssetPathWithMissingLeaf(absPath)\n\t\t\tnotebookRoot, notebookErr := ResolveRealPath(filepath.Join(util.DataDir, resolvedBoxID))\n\t\t\tdataRoot, dataErr := ResolveRealPath(util.DataDir)\n\t\t\tif rootErr != nil || resolveErr != nil || notebookErr != nil || dataErr != nil ||\n\t\t\t\t!gulu.File.IsSubPath(dataRoot, notebookRoot) ||\n\t\t\t\t!gulu.File.IsSubPath(notebookRoot, root) || !gulu.File.IsSubPath(root, resolved) {\n\t\t\t\treturn \"\", errors.New(\"asset path resolves outside notebook assets directory\")\n\t\t\t}\n\t\t}\n\t\tif lookupPath == relativePath {\n\t\t\treturn absPath, nil\n\t\t}\n\t\tcandidates = append(candidates, absPath)\n\t}\n\tsort.Strings(candidates)\n\tif len(candidates) > 0 {\n\t\treturn candidates[0], nil\n\t}\n\treturn \"\", nil\n}\n\n// ensureReadableAssetLocal 先检查加密笔记本准入，再下载原始密文；实际读取仍须认证解密。\nfunc ensureReadableAssetLocal(absPath string) error {\n\tboxID := ExtractBoxIDFromAssetsPath(absPath)\n\tif boxID != \"\" && IsEncryptedBox(boxID) && !IsBoxUnlocked(boxID) {","sourceCodeStart":94,"sourceCodeEnd":130,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/8641553a1f07374001902d3ce773285db1292b2d/kernel/model/asset_download_read.go#L94-L130","documentation":"After resolving a deferred asset path to a real path, deferredAssetPathFromFiles double-checks containment: the resolved file must be inside the data directory, inside the owning notebook's root, and inside the notebook's assets directory (checks via ResolveRealPath and IsSubPath). If any containment check or resolution fails, it returns 'asset path resolves outside notebook assets directory'. Unlike the escape check (604), this catches paths that pass lexical normalization but escape via symlinks or land in another notebook's tree.","triggerScenarios":"deferredAssetPath on an asset whose real path (after symlink resolution) lands outside the notebook's assets dir — e.g. an in-notebook symlink pointing to another notebook's assets, or the notebook root itself fails to resolve; exercised by TestDeferredAssetPathRejectsMissingLeafThroughEscapingSymlink.","commonSituations":"Assets symlinked across notebooks or from outside the data dir; a notebook directory was moved/renamed so stored roots no longer resolve; symlink-heavy setups after restoring a workspace from an archive.","solutions":["Replace cross-notebook symlinks with real files inside the referencing notebook's assets folder","Verify the notebook directory exists and resolves (ResolveRealPath succeeds) — recreate a damaged notebook folder from backup","Move any referenced file that lives outside assets/ into the notebook's assets directory and update the link","Audit data/ for symlinks leaving the workspace and remove them"],"exampleFix":"// before\ndata/notebook1/assets/a.png -> ../notebook2/assets/a.png\n// after\ncp data/notebook2/assets/a.png data/notebook1/assets/a.png  // real file, containment holds","handlingStrategy":"validation","validationCode":"real, err := filepath.EvalSymlinks(absPath)\nif err != nil { return err }\nif !strings.HasPrefix(real, notebookAssetsRoot) {\n    return errors.New(\"asset resolves outside notebook assets\")\n}","typeGuard":null,"tryCatchPattern":"p, err := model.DeferredAssetPath(urlPath)\nif err != nil && strings.Contains(err.Error(), \"outside notebook assets\") {\n    return fmt.Errorf(\"fix asset layout for %q: %w\", urlPath, err)\n}","preventionTips":["Do not symlink assets between notebooks; duplicate the file instead","Keep notebook directories intact — don't move/rename folders behind the app's back","Resolve symlinks (EvalSymlinks) when validating custom asset pipelines","Audit unusual asset layouts after workspace restores"],"tags":["security","symlink","asset-resolution"],"backgroundTag":"path-traversal-blocked","analyzedSha":"8641553a1f07374001902d3ce773285db1292b2d","analyzedAt":"2026-09-11T16:08:28.414Z","contentChangedAt":"2026-09-11T16:08:28.414Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}