siyuan-note/siyuan · error
path is not a child of assets directory: %s
Error message
path is not a child of assets directory: %s
What it means
Thrown by ResolveDataAssetPath (kernel/model/assets.go:945) when the computed absolute path of an asset is not located beneath the assets root directory derived from the same input. It is a defense-in-depth invariant check that runs after the input has already been cleaned and confirmed to contain an `assets` path segment, so under normal operation it should never fire; when it does, it means the path and the detected assets root disagree (typically a malformed or adversarial path that survived earlier cleaning).
Source
Thrown at kernel/model/assets.go:945
if !filelock.IsExist(boxConfPath) {
err = fmt.Errorf("asset path does not belong to a notebook: %s", assetPath)
return
}
if IsEncryptedBox(parts[0]) {
err = fmt.Errorf("accessing assets in encrypted notebook [%s] is not supported", parts[0])
return
}
}
}
if assetDirIndex < 0 {
err = fmt.Errorf("path is not under an assets directory: %s", assetPath)
return
}
assetRootParts := parts[:assetDirIndex+1]
assetRoot := filepath.Join(util.DataDir, filepath.FromSlash(strings.Join(assetRootParts, "/")))
if !gulu.File.IsSubPath(assetRoot, absPath) {
err = fmt.Errorf("path is not a child of assets directory: %s", assetPath)
return
}
resolvedRoot, evalErr := filepath.EvalSymlinks(assetRoot)
if evalErr != nil {
err = fmt.Errorf("resolve assets directory [%s] failed: %w", assetRoot, evalErr)
return
}
if assetDirIndex > 0 {
notebookRoot := filepath.Join(util.DataDir, parts[0])
resolvedDataDir, dataEvalErr := filepath.EvalSymlinks(util.DataDir)
resolvedNotebookRoot, notebookEvalErr := filepath.EvalSymlinks(notebookRoot)
if dataEvalErr != nil || notebookEvalErr != nil ||
!gulu.File.IsSubPath(resolvedDataDir, resolvedNotebookRoot) ||
!gulu.File.IsSubPath(resolvedNotebookRoot, resolvedRoot) {
err = fmt.Errorf("notebook asset path resolves outside notebook directory: %s", assetPath)
return
}View on GitHub (pinned to 251596fc0d)
Solutions
- Inspect the exact assetPath string passed in (it is included via %s) and re-run it through filepath.Clean + filepath.Rel(util.DataDir, ...) manually to see why the assets-root membership check fails.
- Confirm util.DataDir is a real directory and not itself a broken/looping symlink; if it is, fix the workspace bootstrap so DataDir resolves to a stable physical directory.
- If you are a caller building the asset path dynamically, build it with filepath.Join on already-validated segments instead of string concatenation, and ensure it is relative to util.DataDir with an `assets/` prefix.
- Treat occurrences as a security signal: do not silence it, log the raw input and stack trace, and reject the request.
Defensive patterns
Strategy: validation
Validate before calling
// Reject input that cannot possibly be a data-relative asset path before calling ResolveDataAssetPath.
func validDataAssetPath(p string) error {
if p == "" { return errors.New("asset path is required") }
if filepath.IsAbs(p) { return errors.New("asset path must be relative") }
cleaned := filepath.Clean(filepath.FromSlash(p))
rel, err := filepath.Rel(util.DataDir, filepath.Join(util.DataDir, cleaned))
if err != nil || rel == "." || rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
return errors.New("asset path escapes data directory")
}
return nil
} Try / catch
relativePath, absPath, err := model.ResolveDataAssetPath(assetPath)
if err != nil {
// log assetPath for audit; do not fall back to raw filesystem access
logging.LogWarningf("resolve data asset failed: %s, err: %s", assetPath, err)
return "", err
} Prevention
- Always pass a relative path rooted at util.DataDir with an assets/ or <boxID>/assets/ prefix.
- Build asset paths with filepath.Join over validated segments, never string concatenation of user input.
- Do not catch this and retry with a transformed path — treat it as a reject signal.
When it happens
Trigger: Calling model.ResolveDataAssetPath (directly or via cli/cmd/asset.go, mcp/tools/asset.go, server/serve.go:919) with a path string that, after filepath.Clean + filepath.Rel against util.DataDir, contains an `assets` segment but whose resulting absPath is not a descendant of the reconstructed assets root. This is essentially unreachable through ordinary file paths because Clean+Rel already normalize traversal.
Common situations: Developers almost never hit this in production. If observed, it usually indicates either a programmatic caller constructing a non-standard asset path (e.g. embedding NUL bytes or OS-specific separators that confuse Split vs Join), a bug introduced when refactoring the path logic, or a filesystem where DataDir itself is a symlink that changes the Rel result unexpectedly.
Related errors
- path [%s] must not contain '..'
- path [%s] escapes box directory
- asset path must be relative to data directory: %s
- asset path escapes data directory: %s
- asset path resolves outside assets directory: %s
AI-assisted analysis of siyuan-note/siyuan@251596fc0d (2026-08-12).
Data as JSON: /api/errors/22345e85a394a5fd.
Report an issue: GitHub.