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
- Use the exact path strings returned by the history listing APIs instead of building them manually
- Strip/normalize '..' segments and confirm the path is relative to the workspace data/history directory
- Check the path exists under the workspace before calling the rollback APIs
- 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
- Treat historyPath as an opaque token from the API, never user input
- Reject any path containing '..' or absolute prefixes at the UI layer
- Never construct history paths from external/untrusted sources
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
- invalid custom emoji name
- path [%s] must not contain '..'
- path [%s] escapes box directory
- asset path is outside data directory
- invalid package name
AI-assisted analysis of siyuan-note/siyuan@8641553a1f (2026-09-11).
Data as JSON: /api/errors/0f9109f4f57de5ea.
Report an issue: GitHub.