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
- Never forward raw user input as the asset path; whitelist via an asset catalog.
- 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
- Never forward raw user input as the asset path.
- Maintain an allow-list of asset paths from the asset catalog.
- Treat escape attempts as security events, not transient errors.
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
- asset path must be relative to data directory: %s
- path is not a child of assets directory: %s
- [%s] is not an asset path
- [%s] is not an asset path (must start with assets/)
- path escapes templates dir: %s
AI-assisted analysis of siyuan-note/siyuan@251596fc0d (2026-08-12).
Data as JSON: /api/errors/33935d20727789ef.
Report an issue: GitHub.