{"record":{"id":"33935d20727789ef","repo":"siyuan-note/siyuan","slug":"asset-path-escapes-data-directory-s","errorCode":null,"errorMessage":"asset path escapes data directory: %s","messagePattern":"asset path escapes data directory: (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/model/assets.go","lineNumber":1041,"sourceCode":"func ResolveDataAssetPath(assetPath string) (relativePath, absPath string, err error) {\n\tif assetPath == \"\" {\n\t\terr = errors.New(\"asset path is required\")\n\t\treturn\n\t}\n\n\tnativePath := filepath.FromSlash(assetPath)\n\tif filepath.IsAbs(nativePath) || filepath.VolumeName(nativePath) != \"\" ||\n\t\t(len(nativePath) > 0 && os.IsPathSeparator(nativePath[0])) {\n\t\terr = fmt.Errorf(\"asset path must be relative to data directory: %s\", assetPath)\n\t\treturn\n\t}\n\n\tnativePath = filepath.Clean(nativePath)\n\tabsPath = filepath.Join(util.DataDir, nativePath)\n\tdataRelativePath, relErr := filepath.Rel(util.DataDir, absPath)\n\tif relErr != nil || dataRelativePath == \".\" || dataRelativePath == \"..\" ||\n\t\tstrings.HasPrefix(dataRelativePath, \"..\"+string(filepath.Separator)) {\n\t\terr = fmt.Errorf(\"asset path escapes data directory: %s\", assetPath)\n\t\treturn\n\t}\n\n\tparts := strings.Split(filepath.ToSlash(dataRelativePath), \"/\")\n\tassetDirIndex := -1\n\tswitch {\n\tcase len(parts) > 1 && parts[0] == \"assets\":\n\t\tassetDirIndex = 0\n\tcase len(parts) > 2 && ast.IsNodeIDPattern(parts[0]):\n\t\tfor i := 1; i < len(parts)-1; i++ {\n\t\t\tif parts[i] == \"assets\" {\n\t\t\t\tassetDirIndex = i\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif assetDirIndex > 0 {\n\t\t\tboxConfPath := filepath.Join(util.DataDir, parts[0], \".siyuan\", \"conf.json\")\n\t\t\tif !filelock.IsExist(boxConfPath) {","sourceCodeStart":1023,"sourceCodeEnd":1059,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/9f775e8a12daef8255556097396f9b2739078892/kernel/model/assets.go#L1023-L1059","documentation":"After cleaning the input, ResolveDataAssetPath joins it under util.DataDir and computes the path relative to the data directory. If the relative computation fails or the result is \".\", \"..\", or starts with \"../\", the cleaned path escapes the data directory, so the request is rejected as a path-traversal attempt. This protects the workspace from reads/writes outside data/ via crafted paths like \"../conf.json\" or \"a/../../secrets\".","triggerScenarios":"Passing traversal paths such as \"../outside.png\", \"assets/../../etc/passwd\", or \".\" to ResolveDataAssetPath (directly or via assetStat, deferredAssetPathFromFiles, PrepareAgentMessageImage, ResolveUnusedDataAssetPath); also a path that cleans to the data dir itself.","commonSituations":"Malicious or buggy plugin/API input embedding \"..\" segments from user-supplied filenames; joining untrusted URL segments without cleaning; symlinks or relative path arithmetic in scripts that accidentally climb out of the workspace.","solutions":["Remove \"..\" segments and pass a path that stays inside the data directory, e.g. \"assets/img.png\" or \"<notebookID>/assets/img.png\"","Sanitize untrusted input with filepath.Clean and verify the result does not start with \"../\" before calling","If the file truly lives outside the workspace, move or copy it into the global assets/ folder first, then reference it by its data-relative path"],"exampleFix":"// before\nrel, abs, err := model.ResolveDataAssetPath(userInput) // userInput = \"assets/../../secret.txt\"\n// after\ncleaned := path.Clean(\"/\" + strings.ReplaceAll(userInput, \"\\\\\", \"/\"))[1:] // strip traversal\nrel, abs, err := model.ResolveDataAssetPath(cleaned)","handlingStrategy":"validation","validationCode":"cleaned := path.Clean(\"/\" + strings.ReplaceAll(userPath, \"\\\\\", \"/\"))\nif strings.Contains(cleaned, \"../\") {\n    return errors.New(\"path traversal rejected\")\n}\nuserPath = strings.TrimPrefix(cleaned, \"/\")","typeGuard":"func safeRelPath(p string) bool {\n    c := path.Clean(\"/\" + p)\n    return !strings.HasPrefix(c, \"/../\") && c != \"/..\" && !strings.Contains(p, \"..\")\n}","tryCatchPattern":"rel, abs, err := model.ResolveDataAssetPath(p)\nif err != nil {\n    if strings.HasPrefix(err.Error(), \"asset path escapes data directory\") {\n        return fmt.Errorf(\"rejected unsafe asset path %q\", p)\n    }\n    return err\n}","preventionTips":["Always Clean and reject any \"..\" segments in user- or plugin-supplied paths before calling","Treat asset paths from network input as untrusted; whitelist an assets/ prefix","Do not build paths by concatenating raw URL segments; decode and sanitize each segment","Log rejected traversal attempts to detect misuse early"],"tags":["assets","path-traversal","security","validation"],"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"}