{"record":{"id":"0f9109f4f57de5ea","repo":"siyuan-note/siyuan","slug":"history-path-s-is-not-in-workspace","errorCode":null,"errorMessage":"history path [%s] is not in workspace","messagePattern":"history path \\[(.+?)\\] is not in workspace","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/model/history.go","lineNumber":590,"sourceCode":"\t\t\treturn\n\t\t}\n\t}\n\n\tif err = filelock.CopyNewtimes(from, to); err != nil {\n\t\tlogging.LogErrorf(\"copy file [%s] to [%s] failed: %s\", from, to, err)\n\t\treturn\n\t}\n\tIncSync()\n\tutil.PushMsg(Conf.Language(102), 3000)\n\treturn nil\n}\n\n// validateHistoryPath 校验历史路径是否位于工作区内且属于历史目录。\n// 拒绝路径穿越攻击（..、绝对路径等）。返回规范化的绝对路径。\nfunc validateHistoryPath(historyPath string) (string, error) {\n\tp := filepath.Join(util.WorkspaceDir, historyPath)\n\tif !gulu.File.IsSubPath(util.WorkspaceDir, p) {\n\t\treturn \"\", fmt.Errorf(\"history path [%s] is not in workspace\", historyPath)\n\t}\n\tif !gulu.File.IsExist(p) {\n\t\treturn \"\", fmt.Errorf(\"history path [%s] not exist\", historyPath)\n\t}\n\trel, err := filepath.Rel(util.HistoryDir, p)\n\tif err != nil || strings.HasPrefix(rel, \"..\") {\n\t\treturn \"\", fmt.Errorf(\"history path [%s] is not under history directory\", historyPath)\n\t}\n\treturn p, nil\n}\n\n// IsEncryptedHistoryPath 判断历史路径是否明确属于加密笔记本。\nfunc IsEncryptedHistoryPath(absPath string) bool {\n\tboxID := ExtractBoxIDFromHistoryPath(absPath)\n\tif boxID == \"\" {\n\t\treturn false\n\t}\n\tif IsEncryptedBox(boxID) {","sourceCodeStart":572,"sourceCodeEnd":608,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/8641553a1f07374001902d3ce773285db1292b2d/kernel/model/history.go#L572-L608","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\nrollbackDocHistory(\"/etc/passwd\")\nrollbackDocHistory(\"history/../../conf/conf.json\")\n// after\nrollbackDocHistory(\"history/20240101120000-update/20240101120000-xxxxxxx/20240101120000-yyyyyyy.sy\")","handlingStrategy":"validation","validationCode":"function isSafeHistoryPath(historyPath) {\n  const resolved = normalize(workspaceDir + \"/\" + historyPath);\n  return !historyPath.includes(\"..\") && !pathIsAbsolute(historyPath) && resolved.startsWith(workspaceDir);\n}","typeGuard":"null","tryCatchPattern":"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; } }","preventionTips":["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"],"tags":["security","path-traversal","history","validation"],"backgroundTag":"path-traversal-blocked","analyzedSha":"8641553a1f07374001902d3ce773285db1292b2d","analyzedAt":"2026-09-11T16:08:28.414Z","contentChangedAt":"2026-09-11T16:08:28.414Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}