siyuan-note/siyuan · error

asset path escapes data directory: %s

Error message

asset path escapes data directory: %s

What it means

Returned by ResolveDataAssetPath when, after filepath.Join(DataDir, nativePath) and filepath.Rel, the relative result is ".", "..", or begins with "../" — i.e. the input escapes util.DataDir via parent-directory segments. This is the second traversal guard and catches "../"-based escapes that survive filepath.Clean.

Source

Thrown at kernel/model/assets.go:909

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 251596fc0d)

Solutions

  1. Never forward raw user input as the asset path; whitelist via an asset catalog.
  2. If building the path programmatically, assert it stays under the data dir using filepath.Rel before calling.

Example fix

// before
rel, abs, err := model.ResolveDataAssetPath(userInput) // userInput = "../../etc/passwd"

// after — reject escapes before calling
rel, abs, err := model.ResolveDataAssetPath(userInput)
if err != nil { /* user input was unsafe */ }
Defensive patterns

Strategy: validation

Validate before calling

// Pre-check containment before calling the resolver.
abs := filepath.Join(util.DataDir, filepath.Clean(filepath.FromSlash(assetPath)))
rel, err := filepath.Rel(util.DataDir, abs)
if err != nil || rel == "." || rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
    return fmt.Errorf("asset path escapes data directory: %s", assetPath)
}

Prevention

When it happens

Trigger: Passing "../../../../etc/passwd", "..\\..\\secret", or any path whose cleaned, joined form resolves above the data directory. The check fires after Clean, so obfuscations like "a/../../.." are normalized first.

Common situations: Malicious user-controlled path input; a buggy path-join upstream that prepended the wrong base; symlink-unaware code that fed a relative-escape path.

Related errors


AI-assisted analysis of siyuan-note/siyuan@251596fc0d (2026-08-12). Data as JSON: /api/errors/33935d20727789ef. Report an issue: GitHub.