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

  1. Use plain relative paths that stay inside the plugin storage directory
  2. Strip or reject '..' segments and leading separators from dynamic input before calling
  3. Normalize the path in JS first and verify it does not start with '..' or the root
  4. 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

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


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)