siyuan-note/siyuan · error
asset escapes its directory
Error message
asset escapes its directory: %s
What it means
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.
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.
Example fix
// before
RelinkAsset("assets/../../outside.png", "assets/new.png", false)
// after
RelinkAsset("assets/outside.png", "assets/new.png", false) Defensive patterns
Strategy: validation
Validate before calling
real, err := filepath.EvalSymlinks(candidate)
if err != nil {
return err
}
if !gulu.File.IsSubPath(realRoot, real) {
return fmt.Errorf("path escapes root: %s", real)
} Try / catch
if err != nil && strings.HasPrefix(err.Error(), "asset escapes its directory") {
// reject the mapping and log the offending path
} Prevention
- Clean ../ segments from user-supplied paths
- Replace symlinks pointing outside the workspace with copies
- Keep assets inside the notebook's assets directory
When it happens
Trigger: 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.
Common situations: 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.
Understand the failure class
Background: Path traversal blocked: "path escapes the workspace" and "outside site root" errors when a path will not stay inside its allowed directory — this error's family across 26 libraries.
Related errors
- archive entry resolves outside destination
- asset path escapes data directory
- asset path escapes data directory
- asset path is outside data directory
- asset path resolves outside assets directory
AI-assisted analysis of siyuan-note/siyuan@9f775e8a12 (2026-09-19).
Data as JSON: /api/errors/f78b3c40337de8c2.
Report an issue: GitHub.
Appendix: source
Thrown at kernel/model/asset_relink.go:260
for _, root := range roots {
candidate := filepath.Join(root, filepath.FromSlash(strings.TrimPrefix(assetPath, "assets/")))
info, err := os.Stat(candidate)
if os.IsNotExist(err) {
continue
}
if err != nil {
return "", err
}
if !info.Mode().IsRegular() {
return "", fmt.Errorf("asset must be a regular file: %s", assetPath)
}
real, err := filepath.EvalSymlinks(candidate)
if err != nil {
return "", err
}
realRoot, err := filepath.EvalSymlinks(root)
if err != nil || !gulu.File.IsSubPath(realRoot, real) {
return "", fmt.Errorf("asset escapes its directory: %s", assetPath)
}
if err = validateRelinkStoragePath(real); err != nil {
return "", err
}
if IsEncryptedAssetPath(real) {
return "", errors.New("encrypted assets are not supported")
}
if found != "" && found != candidate {
return "", fmt.Errorf("ambiguous asset path: %s", assetPath)
}
found = candidate
}
if required && found == "" {
return "", fmt.Errorf("target asset does not exist locally: %s", assetPath)
}
return found, nil
}
View on GitHub (pinned to 9f775e8a12)