{"record":{"id":"e7fd662be5b87499","repo":"siyuan-note/siyuan","slug":"asset-path-is-outside-data-directory","errorCode":null,"errorMessage":"asset path is outside data directory","messagePattern":"asset path is outside data directory","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/model/asset_download.go","lineNumber":121,"sourceCode":"\tcase conf.ProviderSiYuan:\n\t\tif !IsSubscriber() {\n\t\t\treturn errors.New(Conf.Language(376))\n\t\t}\n\tcase conf.ProviderS3, conf.ProviderWebDAV, conf.ProviderLocal:\n\t\tif !IsPaidUser() {\n\t\t\treturn errors.New(Conf.Language(376))\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc dataRelativeAssetPath(absPath string) (string, error) {\n\tif !filepath.IsAbs(absPath) {\n\t\treturn \"\", fmt.Errorf(\"asset path must be absolute\")\n\t}\n\trel, err := filepath.Rel(util.DataDir, filepath.Clean(absPath))\n\tif err != nil || rel == \"..\" || strings.HasPrefix(rel, \"..\"+string(filepath.Separator)) {\n\t\treturn \"\", fmt.Errorf(\"asset path is outside data directory\")\n\t}\n\tif rel == \".\" {\n\t\treturn \"/\", nil\n\t}\n\treturn \"/\" + filepath.ToSlash(rel), nil\n}\n\n// EnsureAssetLocal 在调用方完成访问校验后补齐资源，下载内容仍由原有读取流程认证。\nfunc EnsureAssetLocal(absPath string) error {\n\tif _, err := os.Stat(absPath); err == nil {\n\t\treturn nil\n\t} else if !errors.Is(err, os.ErrNotExist) {\n\t\treturn err\n\t}\n\tassetDownloadSourceMu.RLock()\n\tdefer assetDownloadSourceMu.RUnlock()\n\trel, err := dataRelativeAssetPath(absPath)\n\tif err != nil {","sourceCodeStart":103,"sourceCodeEnd":139,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/8641553a1f07374001902d3ce773285db1292b2d/kernel/model/asset_download.go#L103-L139","documentation":"dataRelativeAssetPath computes the path relative to util.DataDir and rejects any path that escapes the data directory (result \"..\" or starting with \"../\", or a failed Rel computation). SiYuan throws this as a path-traversal guard so asset download cannot target files outside the workspace data directory.","triggerScenarios":"Calling EnsureAssetLocal or EnsureAssetPrefixLocal with an absolute path that resolves outside util.DataDir, e.g. /etc/passwd or a sibling directory path via .. segments.","commonSituations":"Passing user-supplied paths without sanitization; symlinks resolving outside the data dir; constructing paths from untrusted plugin input; migrating code that used paths from a different workspace.","solutions":["Verify the path is inside util.DataDir before calling (filepath.Rel and prefix check)","Clean the path and remove .. components; reject escape attempts","If the asset legitimately lives elsewhere, copy it into the workspace assets folder first"],"exampleFix":"// before\nmodel.EnsureAssetLocal(\"/etc/hosts\") // outside data dir\n// after\nrel, _ := filepath.Rel(util.DataDir, target)\nif rel == \"..\" || strings.HasPrefix(rel, \"..\") {\n\treturn fmt.Errorf(\"asset outside data dir\")\n}\nmodel.EnsureAssetLocal(target)","handlingStrategy":"validation","validationCode":"rel, err := filepath.Rel(util.DataDir, filepath.Clean(p))\nif err != nil || rel == \"..\" || strings.HasPrefix(rel, \"..\"+string(filepath.Separator)) {\n  return fmt.Errorf(\"path escapes data dir\")\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Sanitize and Clean user/plugin-supplied paths before use","Reject .. segments and symlinks that resolve outside util.DataDir","Keep assets inside the workspace assets directory"],"tags":["path-traversal","security","validation","assets"],"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"}