siyuan-note/siyuan · error

Path [ ] is not in workspace

Error message

Path [%s] is not in workspace

What it means

UploadAssets accepts an optional assetsDirPath telling it where to store files; the resolved absolute path must live inside the current workspace (util.IsAbsPathInWorkspace). Paths that escape the workspace — absolute paths elsewhere on disk, or relative paths whose join traverses out via '..' — are rejected with 'Path [...] is not in workspace' as a security guard against arbitrary file writes.

Solutions

  1. Pass a workspace-relative assetsDirPath such as 'assets' or 'notebook-id/assets', not an absolute OS path
  2. Remove any '..' components from assetsDirPath before sending
  3. Recompute the path against the current workspace (util.DataDir) if the workspace was moved or reconfigured
  4. Validate in the caller: join with the data dir and confirm the result stays prefixed by the workspace path
  5. Omit assetsDirPath entirely to use the default assets directory

Example fix

// before
form.Set("assetsDirPath", "/home/user/elsewhere/assets")
// after
rel := "assets" // or "<boxID>/assets" relative to the workspace data dir
form.Set("assetsDirPath", rel)
Defensive patterns

Strategy: validation

Validate before calling

func safeAssetsDirPath(rel string) error {
    abs := filepath.Join(util.DataDir, rel)
    if !util.IsAbsPathInWorkspace(abs) {
        return fmt.Errorf("assetsDirPath %q escapes the workspace", rel)
    }
    return nil
}

Try / catch

result, msg, err := model.UploadAssets(req)
if err != nil && strings.Contains(err.Error(), "is not in workspace") {
    // fall back to the default: clear assetsDirPath and use workspace assets/
}

Prevention

When it happens

Trigger: POST /api/asset/upload with assetsDirPath set to an absolute path outside the workspace data dir, or a value such as '../other' whose filepath.Join(util.DataDir, ...) escapes the workspace.

Common situations: Scripts/plugins hard-coding an absolute destination from another machine/workspace; moving the workspace without updating a saved assetsDirPath; attempting to write directly to a sibling notebook path outside data/.

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/4b5de3b17f9fe032. Report an issue: GitHub.

Appendix: source

Thrown at kernel/model/upload.go:374

					break
				}
			}
		}
		if nil == bt {
			err = errors.New(Conf.Language(71))
			return
		}
		uploadBoxID = bt.BoxID
		docDirLocalPath := filepath.Join(util.DataDir, bt.BoxID, path.Dir(bt.Path))
		assetsDirPath = getAssetsDir(filepath.Join(util.DataDir, bt.BoxID), docDirLocalPath)
	}

	relAssetsDirPath := "assets"
	if request.AssetsDirPath != nil {
		relAssetsDirPath = *request.AssetsDirPath
		assetsDirPath = filepath.Join(util.DataDir, relAssetsDirPath)
		if !util.IsAbsPathInWorkspace(assetsDirPath) {
			err = errors.New("Path [" + assetsDirPath + "] is not in workspace")
			return
		}
		// assetsDirPath 可能指向加密 box(调用方未传 id),反查 boxID 让文件名脱敏和内容加密生效
		if pathBox := ExtractBoxIDFromAssetsPath(assetsDirPath); pathBox != "" && IsEncryptedBox(pathBox) {
			uploadBoxID = pathBox
			boxAssetsDir := filepath.Join(util.DataDir, pathBox, "assets")
			if rel, relErr := filepath.Rel(boxAssetsDir, assetsDirPath); relErr == nil && rel != ".." &&
				!strings.HasPrefix(rel, ".."+string(os.PathSeparator)) {
				// 加密资源通过 box 查询参数定位,响应转换为 box 内的标准 assets 相对路径。
				relAssetsDirPath = path.Join("assets", filepath.ToSlash(rel))
			}
		}
	}
	if !gulu.File.IsExist(assetsDirPath) {
		if err = os.MkdirAll(assetsDirPath, 0755); err != nil {
			return
		}
	}

View on GitHub (pinned to 9f775e8a12)