joewalnes/websocketd · warning

path %q escapes boundary %q

Error message

path %q escapes boundary %q

What it means

checkPathBoundary resolves symlinks with filepath.EvalSymlinks and verifies the real path still lives inside the resolved boundary directory. If the resolved path starts outside the boundary (e.g. via a symlink pointing elsewhere), it refuses with this error — a security guard against path-traversal and symlink escape.

Source

Thrown at libwebsocketd/handler.go:199

		return urlInfo, nil
	}
	return nil, fmt.Errorf("could not resolve script for path %q", path)
}

// checkPathBoundary resolves symlinks and verifies the real path is within the
// allowed directory. Returns an error if the path escapes the boundary.
func checkPathBoundary(path, boundary string) error {
	realPath, err := filepath.EvalSymlinks(path)
	if err != nil {
		return err
	}
	realBoundary, err := filepath.EvalSymlinks(boundary)
	if err != nil {
		return err
	}
	// Ensure the resolved path starts with the resolved boundary
	if !strings.HasPrefix(realPath, realBoundary+string(filepath.Separator)) && realPath != realBoundary {
		return fmt.Errorf("path %q escapes boundary %q", realPath, realBoundary)
	}
	return nil
}

// generateId produces the per-connection identifier exposed as UNIQUE_ID.
// Crypto-random rather than timestamp-derived: a UnixNano id is guessable
// (one connection's id narrows the next one to nanoseconds) and coarse
// enough to collide under bursts. Falls back to the timestamp only if the
// system CSPRNG is unavailable, which is not a condition worth refusing
// connections over.
func generateId() string {
	b := make([]byte, 8)
	if _, err := rand.Read(b); err != nil {
		return strconv.FormatInt(time.Now().UnixNano(), 10)
	}
	return hex.EncodeToString(b)
}

View on GitHub (pinned to 7a8683dc7f)

Solutions

  1. Remove or retarget the symlink so its real destination is inside the boundary directory
  2. Copy the target files into the boundary instead of symlinking them
  3. Serve the actual parent directory (boundary = the real location of the files) so the symlink stays inside it

Example fix

// before
ln -s /etc/passwd ./cgi/evil        # rejected: escapes boundary
// after
cp /path/to/allowed/script ./cgi/   # real file inside boundary
Defensive patterns

Strategy: validation

Validate before calling

real, err := filepath.EvalSymlinks(filepath.Join(servedDir, name))
if err != nil {
	log.Fatal(err)
}
absBoundary, _ := filepath.EvalSymlinks(servedDir)
if !strings.HasPrefix(real, absBoundary+string(os.PathSeparator)) {
	log.Fatalf("symlink %s escapes served dir", name)
}

Try / catch

if err := checkPathBoundary(p, baseDir); err != nil {
	// 'escapes boundary' — reject request or fix/remove the symlink; do not retry as-is
}

Prevention

When it happens

Trigger: A symlink inside the served script/cgi directory pointing to /etc or any path outside the base dir; a requested path whose symlink chain resolves outside the configured root; TestCgiSymlinkEscape-style setups with `ln -s /etc/passwd <dir>/evil`.

Common situations: Developers symlinking shared scripts from another project into the served directory; dotfile symlinks (e.g. to a home dir) inside a served folder; container images where a symlink target exists on the host but not the container path layout.

Related errors


AI-assisted analysis of joewalnes/websocketd@7a8683dc7f (2026-09-03). Data as JSON: /api/errors/b47137224eb8ccec. Report an issue: GitHub.