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

  1. Remove ".." segments and pass a path that stays inside the data directory, e.g. "assets/img.png" or "<notebookID>/assets/img.png"
  2. Sanitize untrusted input with filepath.Clean and verify the result does not start with "../" before calling
  3. 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

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


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)