joewalnes/websocketd · warning
path %q escapes directory %q
Error message
path %q escapes directory %q
What it means
containsPath computes filepath.Rel between the allowed directory and a candidate child path; if the relative path is '..' or starts with '../', the child is outside the directory and the error is returned. It is a lexical containment check used by resolveCgiPath as defense-in-depth against traversal outside the CGI directory.
Source
Thrown at libwebsocketd/http.go:220
// The rooted clean above should already guarantee this on every OS
// (including Windows, where ToSlash folds "..\" into "../" before the
// clean sees it), so this check is not load-bearing today — it is here
// to fail closed if the normalization above is ever weakened.
if err := containsPath(cgiDir, filePath); err != nil {
return "", err
}
return filePath, nil
}
// containsPath reports an error unless child is dir itself or lies beneath it,
// comparing lexically (no filesystem access, no symlink resolution).
func containsPath(dir, child string) error {
rel, err := filepath.Rel(dir, child)
if err != nil {
return err
}
if rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
return fmt.Errorf("path %q escapes directory %q", child, dir)
}
return nil
}
// serveCGI executes CGI scripts from the configured directory. Returns true if handled.
func (h *WebsocketdServer) serveCGI(w http.ResponseWriter, req *http.Request, log *LogScope) bool {
if h.Config.CgiDir == "" {
return false
}
filePath, err := resolveCgiPath(h.Config.CgiDir, req.URL.Path)
if err != nil {
log.Access("http", "CGI: %s", err)
return false
}
fi, err := os.Stat(filePath)
if err != nil || fi.IsDir() {
return false
}View on GitHub (pinned to 7a8683dc7f)
Solutions
- Normalize untrusted input before joining: path.Clean('/'+filepath.ToSlash(userPath)) so '..' climbs fold to root
- Ensure the child is built by filepath.Join(dir, cleanedName) rather than raw string concatenation
- If you see this in legit use, the child genuinely lies outside dir — fix the caller's path construction, not the check
Example fix
// before
filePath := cgiDir + "/" + urlPath // '..' survives
// after
clean := path.Clean("/" + filepath.ToSlash(urlPath))
filePath := filepath.Join(cgiDir, filepath.FromSlash(clean)) Defensive patterns
Strategy: validation
Validate before calling
clean := path.Clean("/" + filepath.ToSlash(userPath))
if strings.Contains(clean, "..") {
return fmt.Errorf("rejecting traversal attempt: %q", userPath)
}
child := filepath.Join(cgiDir, filepath.FromSlash(clean)) Type guard
func safeChild(dir, userPath string) (string, bool) {
clean := path.Clean("/" + filepath.ToSlash(userPath))
child := filepath.Join(dir, filepath.FromSlash(clean))
rel, err := filepath.Rel(dir, child)
if err != nil || rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
return "", false
}
return child, true
} Try / catch
if err := containsPath(dir, child); err != nil {
// 'escapes directory' — log the rejected input and return 4xx; never retry same input
} Prevention
- Root and Clean all user-supplied paths before joining: path.Clean("/"+p)
- Build child paths with filepath.Join, never string concatenation
- Keep the containsPath check as final defense; do not bypass it in refactors
- Add traversal-attempt tests (../../etc/passwd) to your handler suite
When it happens
Trigger: A joined path like filepath.Join(cgiDir, '../secrets') that Rel shows escaping the dir; calling resolveCgiPath/containsPath with a child built from an unrooted or attacker-controlled URL path containing '..' segments that survive normalization.
Common situations: Hand-rolled path joining from user input without rooting/cleaning first; refactors that bypass the Clean('/'+path) normalization; tests (TestCgiSymlinkEscape) exercising escape attempts.
Related errors
- path %q escapes boundary %q
- no CGI script named in path %q
- same origin policy violated
- origin list matches were not found
AI-assisted analysis of joewalnes/websocketd@7a8683dc7f (2026-09-03).
Data as JSON: /api/errors/49f72c9909f0b7ed.
Report an issue: GitHub.