siyuan-note/siyuan · error

path [%s] escapes box directory

Error message

path [%s] escapes box directory

What it means

Returned by `filesys.ValidateBoxRelativePath` when, after lexical `..` filtering, `gulu.File.IsSubPath(boxRoot, resolved)` reports the resolved path is not inside the box directory. This catches paths that lexically look fine but resolve outside the box root (e.g. via absolute paths or filesystem semantics), closing the gap the `..` check does not cover.

Source

Thrown at kernel/filesys/tree.go:166

// 允许路径以 / 开头(如 /20230101/xxx.sy),会自动标准化再去掉前导斜杠。
// 根路径("/" 或 "")合法,返回空字符串。
func ValidateBoxRelativePath(boxID, p string) (string, error) {
	p = filepath.ToSlash(p)
	// 记录原始路径用于 IsSubPath 校验
	origP := p
	// 标准化:去掉前导 /
	p = strings.TrimPrefix(p, "/")
	// 根路径直接放行(box 根目录本身是合法路径)
	if p == "" {
		return p, nil
	}
	if strings.HasPrefix(p, "..") || strings.Contains(p, "/../") || strings.HasSuffix(p, "/..") || p == ".." || p == "." {
		return "", fmt.Errorf("path [%s] must not contain '..'", origP)
	}
	resolved := filepath.Join(util.DataDir, boxID, origP)
	boxRoot := filepath.Join(util.DataDir, boxID)
	if !gulu.File.IsSubPath(boxRoot, resolved) {
		return "", fmt.Errorf("path [%s] escapes box directory", origP)
	}
	return p, nil
}

func LoadTreeWithFix(boxID, p string, luteEngine *lute.Lute) (ret *parse.Tree, needFix bool, err error) {
	if _, err = ValidateBoxRelativePath(boxID, p); err != nil {
		logging.LogErrorf("invalid tree path [%s] for box [%s]: %s", p, boxID, err)
		return
	}

	dek, encrypted, releaseCryptoLease, leaseErr := acquireCryptoLease(boxID)
	if leaseErr != nil {
		err = leaseErr
		return
	}
	defer releaseCryptoLease()

	rootID := util.GetTreeID(p)

View on GitHub (pinned to 251596fc0d)

Solutions

  1. Use canonical, box-relative paths only; avoid absolute paths in box-relative APIs.
  2. Call `filepath.Clean` and verify the result stays under the box root before invoking tree APIs.
  3. Audit the originating client/sync code to ensure paths are generated server-side from IDs, not from raw input.

Example fix

// before
p := "/" + untrustedPath // may resolve outside box
// after
p := filepath.Clean(untrustedPath)
if rel, err := filepath.Rel(boxRoot, filepath.Join(boxRoot, p)); err != nil || strings.HasPrefix(rel, "..") {
    return errors.New("reject")
}
Defensive patterns

Strategy: validation

Validate before calling

// Defense-in-depth: verify IsSubPath yourself before the call:
resolved := filepath.Join(util.DataDir, boxID, origP)
boxRoot := filepath.Join(util.DataDir, boxID)
if !gulu.File.IsSubPath(boxRoot, resolved) {
    return "", fmt.Errorf("path [%s] escapes box directory", origP)
}

Prevention

When it happens

Trigger: A box-relative path that, once joined with `data/<boxID>/`, lands outside `data/<boxID>` — for example an absolute path component or a value exploiting `IsSubPath` semantics on the host OS.

Common situations: Cross-platform path handling where `filepath.Join` produces an unexpected result; a client supplying a path crafted to slip past the `..` check; symlinks whose target the lexical check does not resolve.

Related errors


AI-assisted analysis of siyuan-note/siyuan@251596fc0d (2026-08-12). Data as JSON: /api/errors/f83563aadfb981a4. Report an issue: GitHub.