siyuan-note/siyuan · warning

[ ] is not sub path of workspace

Error message

[%s] is not sub path of workspace

What it means

After joining <workspace>/data/<boxID>/<relativePath>, GetAssetAbsPathInBox confirms the existing path is a sub-path of the workspace directory before serving it. This error is thrown when the joined path itself lies outside the workspace — typically because DataDir or boxID were redirected, or because the joined string escaped via unusual components. It is a workspace containment guard applied before symlink evaluation.

Solutions

  1. Ensure the data directory physically lives under the workspace directory (DataDir must be a sub-path of WorkspaceDir)
  2. Fix the workspace/data configuration (workspace switcher or CLI flags) so both point at the same tree
  3. If using bind mounts or symlinks for notebooks, mount them inside <workspace>/data/ so containment checks pass

Example fix

// before: data dir outside workspace
util.WorkspaceDir = "/home/u/ws"; util.DataDir = "/mnt/data"
// after: data inside workspace
util.DataDir = filepath.Join(util.WorkspaceDir, "data")
Defensive patterns

Strategy: validation

Validate before calling

p := filepath.Join(util.DataDir, boxID, rel)
if !gulu.File.IsSubPath(util.WorkspaceDir, p) {
	return fmt.Errorf("path escapes workspace")
}

Try / catch

abs, err := model.GetAssetAbsPathInBox(ref, boxID)
if err != nil && strings.Contains(err.Error(), "not sub path of workspace") {
	// fix workspace/data configuration, then retry once
}

Prevention

When it happens

Trigger: Calling GetAssetAbsPathInBox when util.WorkspaceDir/util.DataDir are overridden (e.g. in tests or portable mode) and the constructed path resolves outside the workspace, or when a mounted volume makes the box directory not a filesystem-descendant of WorkspaceDir.

Common situations: Custom test harnesses pointing data at /tmp while WorkspaceDir stays at another root; bind-mounting notebook directories into the workspace from elsewhere; misconfigured SIYUAN_WORKING_DIR or portable-data setups.

Understand the failure class

Background: Path traversal blocked: "path escapes the workspace" and "outside site root" errors when a path will not stay inside its allowed directory — this error's family across 26 libraries.

Related errors


AI-assisted analysis of siyuan-note/siyuan@9f775e8a12 (2026-09-19). Data as JSON: /api/errors/353fc09ca2dfcf24. Report an issue: GitHub.

Appendix: source

Thrown at kernel/model/assets.go:1240

	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")
			if boxID != "" {
				expectedPrefix = filepath.Join(util.DataDir, boxID, "assets")
			}
			if !gulu.File.IsSubPath(expectedPrefix, realP) {
				return "", fmt.Errorf("symlink [%s] resolves outside assets directory: [%s]", p, realP)
			}
		}
		return p, nil
	}
	// 非加密 box 的资源可能回退到全局 data/assets(兼容旧笔记本结构)

View on GitHub (pinned to 9f775e8a12)