siyuan-note/siyuan · error

history path [%s] is not in workspace

Error message

history path [%s] is not in workspace

What it means

validateHistoryPath joins the caller-supplied historyPath onto the workspace directory and rejects the result unless it is a strict sub-path of the workspace. This blocks path traversal (.., absolute paths, symlinks escaping the workspace) before any history file is read.

Source

Thrown at kernel/model/history.go:590

			return
		}
	}

	if err = filelock.CopyNewtimes(from, to); err != nil {
		logging.LogErrorf("copy file [%s] to [%s] failed: %s", from, to, err)
		return
	}
	IncSync()
	util.PushMsg(Conf.Language(102), 3000)
	return nil
}

// validateHistoryPath 校验历史路径是否位于工作区内且属于历史目录。
// 拒绝路径穿越攻击(..、绝对路径等)。返回规范化的绝对路径。
func validateHistoryPath(historyPath string) (string, error) {
	p := filepath.Join(util.WorkspaceDir, historyPath)
	if !gulu.File.IsSubPath(util.WorkspaceDir, p) {
		return "", fmt.Errorf("history path [%s] is not in workspace", historyPath)
	}
	if !gulu.File.IsExist(p) {
		return "", fmt.Errorf("history path [%s] not exist", historyPath)
	}
	rel, err := filepath.Rel(util.HistoryDir, p)
	if err != nil || strings.HasPrefix(rel, "..") {
		return "", fmt.Errorf("history path [%s] is not under history directory", historyPath)
	}
	return p, nil
}

// IsEncryptedHistoryPath 判断历史路径是否明确属于加密笔记本。
func IsEncryptedHistoryPath(absPath string) bool {
	boxID := ExtractBoxIDFromHistoryPath(absPath)
	if boxID == "" {
		return false
	}
	if IsEncryptedBox(boxID) {

View on GitHub (pinned to 8641553a1f)

Solutions

  1. Use the exact path strings returned by the history listing APIs instead of building them manually
  2. Strip/normalize '..' segments and confirm the path is relative to the workspace data/history directory
  3. Check the path exists under the workspace before calling the rollback APIs
  4. Never pass user-controlled input directly as historyPath

Example fix

// before
rollbackDocHistory("/etc/passwd")
rollbackDocHistory("history/../../conf/conf.json")
// after
rollbackDocHistory("history/20240101120000-update/20240101120000-xxxxxxx/20240101120000-yyyyyyy.sy")
Defensive patterns

Strategy: validation

Validate before calling

function isSafeHistoryPath(historyPath) {
  const resolved = normalize(workspaceDir + "/" + historyPath);
  return !historyPath.includes("..") && !pathIsAbsolute(historyPath) && resolved.startsWith(workspaceDir);
}

Type guard

null

Try / catch

try { await rollbackDocHistory(p); } catch (e) { if (String(e.msg).includes("is not in workspace")) { /* reject input as unsafe, log and abort */ } else { throw e; } }

Prevention

When it happens

Trigger: Passing a historyPath containing '..' segments, an absolute path outside the workspace, a Windows drive path, or an empty value to GetDocHistoryContent, RollbackDocHistory, RollbackAssetsHistory, RollbackNotebookHistory, RollbackAttributeViewHistory, or ResolveDocVersionBoxID.

Common situations: Plugins or scripts constructing history paths from untrusted input; API consumers concatenating user input into historyPath; misconfigured sync clients creating odd relative paths; security scanners probing the history APIs.

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@8641553a1f (2026-09-11). Data as JSON: /api/errors/0f9109f4f57de5ea. Report an issue: GitHub.