siyuan-note/siyuan · error
siyuan.storage: path traversal not allowed
Error message
siyuan.storage: path traversal not allowed
What it means
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.
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 251596fc0d)
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.
Example fix
// before
await siyuan.storage.get('../config/settings.json');
// after
await siyuan.storage.get('settings.json'); Defensive patterns
Strategy: validation
Validate before calling
function safeStoragePath(rel) {
if (typeof rel !== 'string' || rel.length === 0) throw new TypeError('storage path required');
const segments = rel.split(/[\\/]+/);
if (segments.some(s => s === '..')) throw new Error('storage path must not contain parent traversal');
return rel.replace(/^[\\/]+/, '');
}
// await siyuan.storage.get(safeStoragePath(userInput)); Type guard
const isSafeRelPath = (p) => typeof p === 'string' && p.length > 0 && !p.split(/[\\/]+/).includes('..') && !/^[A-Za-z]:/.test(p); Try / catch
try { await siyuan.storage.get(rel); }
catch (e) { if (/path traversal not allowed/.test(String(e))) { /* reject user input */ } else throw e; } Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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).
Related errors
- marketplace package contains an invalid path
- path escapes templates dir: %s
- path [%s] must not contain '..'
- path [%s] escapes box directory
- path escapes workspace: %s
AI-assisted analysis of siyuan-note/siyuan@251596fc0d (2026-08-12).
Data as JSON: /api/errors/c18c071071c17f2f.
Report an issue: GitHub.