{"record":{"id":"f78b3c40337de8c2","repo":"siyuan-note/siyuan","slug":"asset-escapes-its-directory-s","errorCode":null,"errorMessage":"asset escapes its directory: %s","messagePattern":"asset escapes its directory: (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/model/asset_relink.go","lineNumber":260,"sourceCode":"\tfor _, root := range roots {\n\t\tcandidate := filepath.Join(root, filepath.FromSlash(strings.TrimPrefix(assetPath, \"assets/\")))\n\t\tinfo, err := os.Stat(candidate)\n\t\tif os.IsNotExist(err) {\n\t\t\tcontinue\n\t\t}\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t\tif !info.Mode().IsRegular() {\n\t\t\treturn \"\", fmt.Errorf(\"asset must be a regular file: %s\", assetPath)\n\t\t}\n\t\treal, err := filepath.EvalSymlinks(candidate)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t\trealRoot, err := filepath.EvalSymlinks(root)\n\t\tif err != nil || !gulu.File.IsSubPath(realRoot, real) {\n\t\t\treturn \"\", fmt.Errorf(\"asset escapes its directory: %s\", assetPath)\n\t\t}\n\t\tif err = validateRelinkStoragePath(real); err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t\tif IsEncryptedAssetPath(real) {\n\t\t\treturn \"\", errors.New(\"encrypted assets are not supported\")\n\t\t}\n\t\tif found != \"\" && found != candidate {\n\t\t\treturn \"\", fmt.Errorf(\"ambiguous asset path: %s\", assetPath)\n\t\t}\n\t\tfound = candidate\n\t}\n\tif required && found == \"\" {\n\t\treturn \"\", fmt.Errorf(\"target asset does not exist locally: %s\", assetPath)\n\t}\n\treturn found, nil\n}\n","sourceCodeStart":242,"sourceCodeEnd":278,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/9f775e8a12daef8255556097396f9b2739078892/kernel/model/asset_relink.go#L242-L278","documentation":"After resolving symlinks, the engine verifies the resolved asset stays inside its expected root via gulu.File.IsSubPath(realRoot, real). If EvalSymlinks fails, or the resolved real path lies outside the root (path escape), resolution aborts. This prevents relinking to or through paths that leave the notebook/storage tree.","triggerScenarios":"An asset path (or a symlink it traverses) resolves outside the permitted root directory — e.g. '../../secret.txt' style traversal, or 'assets/link.png' pointing to /etc/passwd; also raised when filepath.EvalSymlinks on the root itself errors; invoked from scanMetadata.","commonSituations":"Symlinked assets that point into other notebooks or system locations; user-supplied paths containing ../; notebooks relocated with dangling or external links; automated tooling building paths by naive string concatenation.","solutions":["Remove ../ traversal components so the path stays inside the assets root.","Replace symlinked assets with real copies inside the assets directory.","Ensure the notebook and its assets live under the expected root and that EvalSymlinks on the root succeeds (path exists, permissions OK).","Run with dryRun=true and validate all mapping paths before relinking."],"exampleFix":"// before\nRelinkAsset(\"assets/../../outside.png\", \"assets/new.png\", false)\n// after\nRelinkAsset(\"assets/outside.png\", \"assets/new.png\", false)","handlingStrategy":"validation","validationCode":"real, err := filepath.EvalSymlinks(candidate)\nif err != nil {\n    return err\n}\nif !gulu.File.IsSubPath(realRoot, real) {\n    return fmt.Errorf(\"path escapes root: %s\", real)\n}","typeGuard":null,"tryCatchPattern":"if err != nil && strings.HasPrefix(err.Error(), \"asset escapes its directory\") {\n    // reject the mapping and log the offending path\n}","preventionTips":["Clean ../ segments from user-supplied paths","Replace symlinks pointing outside the workspace with copies","Keep assets inside the notebook's assets directory"],"tags":["security","path-traversal","asset-relink"],"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"}