{"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/9f775e8a12daef8255556097396f9b2739078892/kernel/plugin/api_storage.go#L25-L61","documentation":"The siyuan.storage API resolves plugin-supplied relative paths against the plugin's storage directory. After filepath.Join/Clean, if the absolute path escapes that directory (no prefix match), the call is rejected with 'siyuan.storage: path traversal not allowed'. This is a security guard preventing plugins from reading/writing outside their sandbox via sequences like ../.","triggerScenarios":"Calling any siyuan.storage method with a path containing '..' segments that resolve above storageDir, an absolute path, or a symlink-cleaned path outside the plugin directory.","commonSituations":"Joining user input or document IDs into storage paths without sanitizing; passing OS-rooted paths; migrating code that previously used arbitrary filesystem paths.","solutions":["Use plain relative paths that stay inside the plugin storage directory","Strip or reject '..' segments and leading separators from dynamic input before calling","Normalize the path in JS first and verify it does not start with '..' or the root","Store files under names derived from sanitized IDs (encodeURIComponent, etc.)"],"exampleFix":"// before\nawait siyuan.storage.get('../../conf/conf.json')\n// after\nconst safe = String(name).replace(/[^a-zA-Z0-9._-]/g, '_')\nawait siyuan.storage.get('cache/' + safe)","handlingStrategy":"validation","validationCode":"function safeRel(p) {\n  if (typeof p !== 'string' || p.startsWith('/') || p.includes('\\\\')) throw new Error('bad path')\n  const norm = p.split('/').filter(s => s && s !== '.')\n  if (norm.includes('..')) throw new Error('path traversal not allowed')\n  return norm.join('/')\n}\nawait siyuan.storage.get(safeRel(userInput))","typeGuard":"const isSafeRel = (p) => typeof p === 'string' && !p.startsWith('/') && !p.split(/[\\\\/]/).includes('..')","tryCatchPattern":"try { await siyuan.storage.get(p) } catch (e) { if (e.message.includes('path traversal not allowed')) { /* sanitize and retry or reject input */ } throw e }","preventionTips":["Sanitize all dynamic path segments (encodeURIComponent or allowlist regex)","Never concatenate raw user/document input into storage paths","Keep all storage under plain relative names inside the plugin dir","Test paths containing '..' and absolute forms in plugin tests"],"tags":["security","path-traversal","storage"],"backgroundTag":"path-traversal-blocked","analyzedSha":"9f775e8a12daef8255556097396f9b2739078892","analyzedAt":"2026-09-19T03:17:15.984Z","contentChangedAt":"2026-09-19T03:17:15.984Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}