{"record":{"id":"4bfcb084c67d8e25","repo":"siyuan-note/siyuan","slug":"asset-path-escapes-data-directory","errorCode":null,"errorMessage":"asset path escapes data directory","messagePattern":"asset path escapes data directory","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/model/asset_download_read.go","lineNumber":94,"sourceCode":"\t\t\t}\n\t\t\tlookupPath = target\n\t\t} else {\n\t\t\tswitch {\n\t\t\tcase candidate == relativePath, strings.HasSuffix(candidate, \"/\"+relativePath):\n\t\t\tcase strings.HasPrefix(candidate, relativePath+\"/\"):\n\t\t\t\tlookupPath = relativePath\n\t\t\tcase strings.Contains(candidate, \"/\"+relativePath+\"/\"):\n\t\t\t\tlookupPath = candidate[:strings.Index(candidate, \"/\"+relativePath+\"/\")+len(relativePath)+1]\n\t\t\tdefault:\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\t\tabsPath := filepath.Join(util.DataDir, filepath.FromSlash(lookupPath))\n\t\tif !includeEncrypted && IsEncryptedAssetPath(absPath) {\n\t\t\tcontinue\n\t\t}\n\t\tif !gulu.File.IsSubPath(util.DataDir, absPath) {\n\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\")","sourceCodeStart":76,"sourceCodeEnd":112,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/8641553a1f07374001902d3ce773285db1292b2d/kernel/model/asset_download_read.go#L76-L112","documentation":"deferredAssetPathFromFiles builds an absolute path under util.DataDir from a candidate lookup path and verifies it is a strict sub-path of the data directory with gulu.File.IsSubPath. If the resolved candidate escapes DataDir (e.g. via ../ or an absolute path from another location), it returns 'asset path escapes data directory'. This is a path-traversal guard protecting the deferred asset download pipeline from reading or downloading outside the workspace.","triggerScenarios":"A document references an asset URL whose normalized lookup path contains .. segments or resolves outside the workspace (deferredAssetPath / deferredAssetPathFromFiles), including crafted markdown like ![x](../../outside/secret).","commonSituations":"Markdown imported from external tools contains relative paths that climb above the notebook; a plugin or third-party sync inserted absolute asset links pointing elsewhere on disk; malicious or mistyped asset links in imported documents.","solutions":["Fix the asset reference in the document to a workspace-relative assets/... path","Copy the referenced file into the notebook's assets folder and update the link","Sanitize/normalize asset URLs at import time so they never contain .. segments","If a legitimate asset lives outside the workspace, move it into data/ rather than linking across"],"exampleFix":"// before (in markdown)\n![img](../../../etc/passwd)\n// after\n![img](assets/passwd.png) // file actually placed under the notebook's assets dir","handlingStrategy":"validation","validationCode":"func safeAssetPath(urlPath string) error {\n    cleaned := path.Clean(\"/\" + strings.ReplaceAll(urlPath, \"\\\\\", \"/\"))\n    if strings.Contains(cleaned, \"..\") {\n        return errors.New(\"asset url contains traversal segments\")\n    }\n    return nil\n}","typeGuard":null,"tryCatchPattern":"p, err := model.DeferredAssetPath(urlPath)\nif err != nil && strings.Contains(err.Error(), \"escapes data directory\") {\n    log.Warnf(\"rejected escaping asset link %q\", urlPath)\n    return nil // treat as unresolvable, skip download\n}","preventionTips":["Reject asset URLs containing ../ before inserting them into documents","Sanitize asset links at import/clip time","Keep all assets inside the notebook's assets/ folder","Review plugins/extensions that rewrite asset paths for traversal"],"tags":["security","path-traversal","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-14T05:17:10.506Z"}