siyuan-note/siyuan · error
asset path escapes data directory
Error message
asset path escapes data directory: %s
What it means
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".
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
Example fix
// before
rel, abs, err := model.ResolveDataAssetPath(userInput) // userInput = "assets/../../secret.txt"
// after
cleaned := path.Clean("/" + strings.ReplaceAll(userInput, "\\", "/"))[1:] // strip traversal
rel, abs, err := model.ResolveDataAssetPath(cleaned) Defensive patterns
Strategy: validation
Validate before calling
cleaned := path.Clean("/" + strings.ReplaceAll(userPath, "\\", "/"))
if strings.Contains(cleaned, "../") {
return errors.New("path traversal rejected")
}
userPath = strings.TrimPrefix(cleaned, "/") Type guard
func safeRelPath(p string) bool {
c := path.Clean("/" + p)
return !strings.HasPrefix(c, "/../") && c != "/.." && !strings.Contains(p, "..")
} Try / catch
rel, abs, err := model.ResolveDataAssetPath(p)
if err != nil {
if strings.HasPrefix(err.Error(), "asset path escapes data directory") {
return fmt.Errorf("rejected unsafe asset path %q", p)
}
return err
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
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
- asset path escapes data directory
- asset path is outside data directory
- asset path [ ] is outside assets directory [ ]
- export path is outside export directory
- history path [ ] is not in workspace
AI-assisted analysis of siyuan-note/siyuan@9f775e8a12 (2026-09-19).
Data as JSON: /api/errors/33935d20727789ef.
Report an issue: GitHub.
Appendix: source
Thrown at kernel/model/assets.go:1041
func ResolveDataAssetPath(assetPath string) (relativePath, absPath string, err error) {
if assetPath == "" {
err = errors.New("asset path is required")
return
}
nativePath := filepath.FromSlash(assetPath)
if filepath.IsAbs(nativePath) || filepath.VolumeName(nativePath) != "" ||
(len(nativePath) > 0 && os.IsPathSeparator(nativePath[0])) {
err = fmt.Errorf("asset path must be relative to data directory: %s", assetPath)
return
}
nativePath = filepath.Clean(nativePath)
absPath = filepath.Join(util.DataDir, nativePath)
dataRelativePath, relErr := filepath.Rel(util.DataDir, absPath)
if relErr != nil || dataRelativePath == "." || dataRelativePath == ".." ||
strings.HasPrefix(dataRelativePath, ".."+string(filepath.Separator)) {
err = fmt.Errorf("asset path escapes data directory: %s", assetPath)
return
}
parts := strings.Split(filepath.ToSlash(dataRelativePath), "/")
assetDirIndex := -1
switch {
case len(parts) > 1 && parts[0] == "assets":
assetDirIndex = 0
case len(parts) > 2 && ast.IsNodeIDPattern(parts[0]):
for i := 1; i < len(parts)-1; i++ {
if parts[i] == "assets" {
assetDirIndex = i
break
}
}
if assetDirIndex > 0 {
boxConfPath := filepath.Join(util.DataDir, parts[0], ".siyuan", "conf.json")
if !filelock.IsExist(boxConfPath) {View on GitHub (pinned to 9f775e8a12)