{"record":{"id":"c18c071071c17f2f","repo":"siyuan-note/siyuan","slug":"siyuan-storage-path-traversal-not-allowed","errorCode":null,"errorMessage":"siyuan.storage: path traversal not allowed","messagePattern":"siyuan\\.storage: path traversal not allowed","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/plugin/api_storage.go","lineNumber":43,"sourceCode":"\t\"github.com/dop251/goja\"\n\t\"github.com/samber/lo\"\n\t\"github.com/siyuan-note/filelock\"\n\t\"github.com/siyuan-note/logging\"\n\t\"github.com/siyuan-note/siyuan/kernel/util\"\n)\n\n// injectStorage adds siyuan.storage.* methods for scoped file CRUD.\nfunc injectStorage(p *KernelPlugin, rt *goja.Runtime, siyuan *goja.Object) (err error) {\n\tdefer func() {\n\t\tif r := recover(); r != nil {\n\t\t\terr = fmt.Errorf(\"injectStorage: %v\", r)\n\t\t}\n\t}()\n\n\tresolvePath := func(relPath string) (abs string, err error) {\n\t\tabs = filepath.Join(p.storageDir, filepath.Clean(relPath))\n\t\tif !(abs == p.storageDir || strings.HasPrefix(abs, p.storageDir+string(filepath.Separator))) {\n\t\t\terr = fmt.Errorf(\"siyuan.storage: path traversal not allowed\")\n\t\t}\n\t\treturn\n\t}\n\n\twatcher := rt.NewObject()\n\n\t// siyuan.storage.watcher.add(path) -> Promise<void>\n\tlo.Must0(watcher.Set(\"add\", rt.ToValue(func(call goja.FunctionCall, rt *goja.Runtime) goja.Value {\n\t\tpromise, resolve, reject := rt.NewPromise()\n\n\t\tvar argErr error\n\t\tvar path string\n\t\tif len(call.Arguments) >= 1 && goja.IsString(call.Argument(0)) {\n\t\t\tpath = call.Argument(0).String()\n\t\t} else {\n\t\t\targErr = fmt.Errorf(\"path required\")\n\t\t}\n","sourceCodeStart":25,"sourceCodeEnd":61,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/251596fc0de2f9528c00c224252fd073a99973f4/kernel/plugin/api_storage.go#L25-L61","documentation":"Thrown by the internal resolvePath helper used by every siyuan.storage.* method (get, put, remove, watcher.add, watcher.remove). It joins the plugin-supplied relative path under p.storageDir and then verifies the cleaned result still lives inside that directory; any path that escapes the sandbox is rejected. This is a security boundary preventing plugins from reading or writing arbitrary kernel/user files.","triggerScenarios":"Passing a path containing parent traversal segments that escape the root after filepath.Clean: '../secret', '../../etc/passwd', '/absolute/path', 'data/../../conf'. On Windows, drive-prefixed or UNC paths also resolve outside the storage dir.","commonSituations":"Plugin naively concatenates user input into the path; a plugin stores a relative path that was later moved; use of absolute paths assuming storage is the FS root; symlinks inside storage that point outside (Clean does not resolve symlinks, but Join semantics can still trip).","solutions":["Use only simple relative paths (e.g. 'notes/x.json') and never prepend '/' or '..'.","Sanitize user-supplied path segments: reject any segment equal to '..' and strip leading slashes.","Keep all storage under subfolders of the plugin's storage dir and treat that dir as the virtual root."],"exampleFix":"// before\nawait siyuan.storage.get('../config/settings.json');\n// after\nawait siyuan.storage.get('settings.json');","handlingStrategy":"validation","validationCode":"function safeStoragePath(rel) {\n  if (typeof rel !== 'string' || rel.length === 0) throw new TypeError('storage path required');\n  const segments = rel.split(/[\\\\/]+/);\n  if (segments.some(s => s === '..')) throw new Error('storage path must not contain parent traversal');\n  return rel.replace(/^[\\\\/]+/, '');\n}\n// await siyuan.storage.get(safeStoragePath(userInput));","typeGuard":"const isSafeRelPath = (p) => typeof p === 'string' && p.length > 0 && !p.split(/[\\\\/]+/).includes('..') && !/^[A-Za-z]:/.test(p);","tryCatchPattern":"try { await siyuan.storage.get(rel); }\ncatch (e) { if (/path traversal not allowed/.test(String(e))) { /* reject user input */ } else throw e; }","preventionTips":["Treat the storage dir as a virtual root; never prepend '/' or '..'.","Reject user-supplied segments equal to '..' before they reach the API.","Avoid absolute and drive/UNC paths in plugin storage calls."],"tags":["plugin-api","storage","security","path-traversal","sandbox"],"backgroundTag":null,"analyzedSha":"251596fc0de2f9528c00c224252fd073a99973f4","analyzedAt":"2026-08-12T21:18:37.123Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}