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

  1. Always pass the canonical notebook ID (obtain via model.ListNotebooks or the box's conf.json), never the notebook name.
  2. If the box came from a URL query param, validate it with ast.IsNodeIDPattern before calling.
  3. If no specific box is intended, pass an empty string rather than a placeholder.
  4. 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

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


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