{"record":{"id":"ba18f766b1825196","repo":"siyuan-note/siyuan","slug":"history-path-s-is-not-under-history-directory","errorCode":null,"errorMessage":"history path [%s] is not under history directory","messagePattern":"history path \\[(.+?)\\] is not under history directory","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/model/history.go","lineNumber":597,"sourceCode":"\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) {\n\t\treturn true\n\t}\n\trel, err := filepath.Rel(util.HistoryDir, absPath)\n\tif err != nil {\n\t\treturn false\n\t}\n\tparts := strings.SplitN(filepath.ToSlash(rel), \"/\", 3)","sourceCodeStart":579,"sourceCodeEnd":615,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/8641553a1f07374001902d3ce773285db1292b2d/kernel/model/history.go#L579-L615","documentation":"After confirming the path is inside the workspace and exists, validateHistoryPath computes its path relative to util.HistoryDir and rejects it if the relative path escapes the history directory (starts with '..') or Rel fails. Only files genuinely under data/history may be used as history paths, preventing misuse of arbitrary workspace files as history sources.","triggerScenarios":"Passing a workspace path that exists but is not under data/history — e.g. data/documents, data/assets, data/storage/av files directly, or a path resolving via symlink outside the history tree, to any of the history APIs.","commonSituations":"Confusing the live document path with its history snapshot; passing a storage/av live JSON instead of its history copy; scripting rollbacks with workspace-relative doc paths; attempting to 'restore' an asset by pointing at the live assets directory.","solutions":["Use only paths whose prefix is the workspace data/history directory as returned by history listing APIs","Do not substitute live-file paths for history paths; locate the corresponding snapshot under data/history","Rebuild/refresh the history index if the expected snapshot path resolves elsewhere"],"exampleFix":"// before: live path, not a history snapshot\nrollbackAttributeViewHistory(\"data/storage/av/view.json\")\n// after: history snapshot under data/history\nrollbackAttributeViewHistory(\"data/history/20240101120000-update/<boxID>/storage/av/view.json\")","handlingStrategy":"validation","validationCode":"function isUnderHistoryDir(historyPath) {\n  const normalized = historyPath.replace(/\\\\/g, \"/\");\n  return normalized.startsWith(\"history/\") || normalized.includes(\"/history/\");\n}","typeGuard":"null","tryCatchPattern":"try { await rollbackDocHistory(p); } catch (e) { if (String(e.msg).includes(\"not under history directory\")) { /* switch to the appropriate live-file API or locate the snapshot */ } else { throw e; } }","preventionTips":["Remember history APIs only accept data/history/... paths","Use separate live-file APIs for working documents and assets","Match each history item type to its dedicated rollback endpoint"],"tags":["history","path-validation","security"],"backgroundTag":"invalid-argument-format","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"}