siyuan-note/siyuan · error

[%s] is not an asset path

Error message

[%s] is not an asset path

What it means

Thrown by GetAssetAbsPathInBox (kernel/model/assets.go:1081) when, after path.Clean, the relativePath equals `.`, `..`, begins with `../`, or is absolute. It is the first line of path-traversal defense in the box-scoped resolver, rejecting empty or escaping input before any filesystem access.

Source

Thrown at kernel/model/assets.go:1081

			}
		}
	}
	cleanPath = filepath.ToSlash(relativePath)
	return
}

// 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)
		}
		// 解析符号链接/目录联接,防止软链接跳出资产根目录

View on GitHub (pinned to 251596fc0d)

Solutions

  1. Validate and normalize the path on the trust boundary before calling: reject anything that is absolute or contains `..`.
  2. Ensure the input begins with `assets/` and is relative (see also error 408).
  3. If the value came from an HTTP request, treat this as a likely attack and return 400 without retry.
  4. For programmatic callers, build paths with filepath.Join from trusted segments rather than echoing raw input.

Example fix

// before
abs, err := model.GetAssetAbsPathInBox(userInput, box)

// after — validate at the boundary
if path.IsAbs(userInput) || strings.Contains(userInput, "..") {
    return fmt.Errorf("invalid asset path: %q", userInput)
}
abs, err := model.GetAssetAbsPathInBox(userInput, box)
Defensive patterns

Strategy: validation

Validate before calling

// Reject traversal/absolute/empty input at the trust boundary.
func isSafeRelativeAssetPath(p string) error {
    c := path.Clean(filepath.ToSlash(p))
    if c == "." || c == ".." || strings.HasPrefix(c, "../") || path.IsAbs(c) {
        return fmt.Errorf("unsafe asset path: %q", p)
    }
    return nil
}

Type guard

func isRelativeAssetPath(p string) bool {
    c := path.Clean(filepath.ToSlash(p))
    return c != "." && c != ".." && !strings.HasPrefix(c, "../") && !path.IsAbs(c)
}

Prevention

When it happens

Trigger: Calling GetAssetAbsPathInBox (the box-scoped resolver used by api/asset.go, api/file.go, api/clipboard.go, model/export.go, model/transaction.go, mcp/tools/image.go, server/serve.go:952) with input like `../etc/passwd`, `..`, an empty-after-clean string, or an absolute path such as `/data/assets/x.png`.

Common situations: An HTTP request (e.g. /api/asset/file or /api/file/copyFile) with a crafted path param; a plugin/MCP tool passing a user-supplied path unvalidated; URL-decoded input that contained `%2e%2e/`.

Related errors


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