siyuan-note/siyuan · error
[%s] is not a box id
Error message
[%s] is not a box id
What it means
Thrown by GetAssetAbsPathInBox (kernel/model/assets.go:1087) when a non-empty boxID does not match ast.IsNodeIDPattern — SiYuan's notebook ID format (a timestamp-style identifier like `20250812143022-abcdefg`). It rejects malformed box IDs (whether supplied by the caller or parsed from a `?box=` query param) before they are joined into a filesystem path.
Source
Thrown at kernel/model/assets.go:1087
// GetAssetAbsPathInBox 在指定 box 内解析资源绝对路径,不进行全局遍历。
// relativePath 必须以 assets/ 前缀开头,boxID 为空且路径没有 box 查询参数时只解析普通/全局资源,不遍历加密 box。
// 加密 box 直接从 <boxID>/assets/ 查找,不依赖后缀匹配。
func GetAssetAbsPathInBox(relativePath, boxID string) (string, error) {
var err error
relativePath, boxID, err = assetPathAndBox(relativePath, boxID)
if err != nil {
return "", err
}
relativePath = path.Clean(relativePath)
if relativePath == "." || strings.HasPrefix(relativePath, "../") || relativePath == ".." || path.IsAbs(relativePath) {
return "", fmt.Errorf("[%s] is not an asset path", relativePath)
}
if !strings.HasPrefix(relativePath, "assets/") {
return "", fmt.Errorf("[%s] is not an asset path (must start with assets/)", relativePath)
}
if boxID != "" && !ast.IsNodeIDPattern(boxID) {
return "", fmt.Errorf("[%s] is not a box id", boxID)
}
if boxID == "" {
return GetAssetAbsPathWithOpt(relativePath, false)
}
p := filepath.Join(util.DataDir, boxID, relativePath)
if gulu.File.IsExist(p) {
if !gulu.File.IsSubPath(util.WorkspaceDir, p) {
return "", fmt.Errorf("[%s] is not sub path of workspace", p)
}
// 解析符号链接/目录联接,防止软链接跳出资产根目录
if realP, evalErr := filepath.EvalSymlinks(p); evalErr == nil && realP != p {
if !gulu.File.IsSubPath(util.WorkspaceDir, realP) {
return "", fmt.Errorf("symlink [%s] resolves outside workspace: [%s]", p, realP)
}
// 验证解析后的路径仍在 <boxID>/assets/ 或全局 data/assets/ 下
expectedPrefix := filepath.Join(util.DataDir, "assets")View on GitHub (pinned to 251596fc0d)
Solutions
- Always pass the canonical notebook ID (obtain via model.ListNotebooks or the box's conf.json), never the notebook name.
- If the box came from a URL query param, validate it with ast.IsNodeIDPattern before calling.
- If no specific box is intended, pass an empty string rather than a placeholder.
- Treat malformed box IDs from HTTP input as a likely attack and return 400.
Example fix
// before
abs, err := model.GetAssetAbsPathInBox(rel, boxFromUser)
// after
if boxFromUser != "" && !ast.IsNodeIDPattern(boxFromUser) {
return "", fmt.Errorf("invalid box id: %q", boxFromUser)
}
abs, err := model.GetAssetAbsPathInBox(rel, boxFromUser) Defensive patterns
Strategy: validation
Validate before calling
import "github.com/88250/lute/ast"
if boxID != "" && !ast.IsNodeIDPattern(boxID) {
return fmt.Errorf("invalid box id: %q", boxID)
} Type guard
func isValidBoxID(boxID string) bool {
return boxID == "" || ast.IsNodeIDPattern(boxID)
} Prevention
- Always use the notebook's canonical ID (from ListNotebooks), not its name or slug.
- Validate box IDs parsed from URL query params with ast.IsNodeIDPattern.
- Pass an empty string rather than a placeholder when no box is intended.
When it happens
Trigger: Calling GetAssetAbsPathInBox(path, boxID) where boxID is something like `../foo`, `assets`, an empty-ish string of spaces, or any value that does not fit the node-ID grammar. Also reached when a `?box=` query param in relativePath holds a garbage value (because assetPathAndBox promotes it into boxID).
Common situations: Frontend/HTTP caller passing the notebook name instead of its ID; a crafted `?box=` query param used as a traversal vector (e.g. `?box=..`); stale code that passes a human-readable slug.
Related errors
- box mismatch: caller specified [%s] but URL has [%s]
- assetPath is not an image referenced by the document
- asset path must be relative to data directory: %s
- asset path escapes data directory: %s
- path is not a child of assets directory: %s
AI-assisted analysis of siyuan-note/siyuan@251596fc0d (2026-08-12).
Data as JSON: /api/errors/6544da5e98e52067.
Report an issue: GitHub.