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
- Remove or retarget the symlink so its real destination is inside the boundary directory
- Copy the target files into the boundary instead of symlinking them
- 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
- Do not symlink external files into served directories; copy them
- Audit served dirs for symlinks: `find . -type l -ls`
- Keep the boundary at the real parent of all served files
- Treat this error as a security signal, not a bug to route around
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.