siyuan-note/siyuan · error
siyuan.storage: path traversal not allowed
Error message
siyuan.storage: path traversal not allowed
What it means
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 ../.
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.)
Example fix
// before
await siyuan.storage.get('../../conf/conf.json')
// after
const safe = String(name).replace(/[^a-zA-Z0-9._-]/g, '_')
await siyuan.storage.get('cache/' + safe) Defensive patterns
Strategy: validation
Validate before calling
function safeRel(p) {
if (typeof p !== 'string' || p.startsWith('/') || p.includes('\\')) throw new Error('bad path')
const norm = p.split('/').filter(s => s && s !== '.')
if (norm.includes('..')) throw new Error('path traversal not allowed')
return norm.join('/')
}
await siyuan.storage.get(safeRel(userInput)) Type guard
const isSafeRel = (p) => typeof p === 'string' && !p.startsWith('/') && !p.split(/[\\/]/).includes('..') Try / catch
try { await siyuan.storage.get(p) } catch (e) { if (e.message.includes('path traversal not allowed')) { /* sanitize and retry or reject input */ } throw e } Prevention
- 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
When it happens
Trigger: 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.
Common situations: Joining user input or document IDs into storage paths without sanitizing; passing OS-rooted paths; migrating code that previously used arbitrary filesystem paths.
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
- archive entry resolves outside destination
- asset escapes its directory
- asset path escapes data directory
- asset path escapes data directory
- asset path is outside data directory
AI-assisted analysis of siyuan-note/siyuan@9f775e8a12 (2026-09-19).
Data as JSON: /api/errors/c18c071071c17f2f.
Report an issue: GitHub.
Appendix: source
Thrown at kernel/plugin/api_storage.go:43
"github.com/dop251/goja"
"github.com/samber/lo"
"github.com/siyuan-note/filelock"
"github.com/siyuan-note/logging"
"github.com/siyuan-note/siyuan/kernel/util"
)
// injectStorage adds siyuan.storage.* methods for scoped file CRUD.
func injectStorage(p *KernelPlugin, rt *goja.Runtime, siyuan *goja.Object) (err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("injectStorage: %v", r)
}
}()
resolvePath := func(relPath string) (abs string, err error) {
abs = filepath.Join(p.storageDir, filepath.Clean(relPath))
if !(abs == p.storageDir || strings.HasPrefix(abs, p.storageDir+string(filepath.Separator))) {
err = fmt.Errorf("siyuan.storage: path traversal not allowed")
}
return
}
watcher := rt.NewObject()
// siyuan.storage.watcher.add(path) -> Promise<void>
lo.Must0(watcher.Set("add", rt.ToValue(func(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
promise, resolve, reject := rt.NewPromise()
var argErr error
var path string
if len(call.Arguments) >= 1 && goja.IsString(call.Argument(0)) {
path = call.Argument(0).String()
} else {
argErr = fmt.Errorf("path required")
}
View on GitHub (pinned to 9f775e8a12)