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

  1. Normalize untrusted input before joining: path.Clean('/'+filepath.ToSlash(userPath)) so '..' climbs fold to root
  2. Ensure the child is built by filepath.Join(dir, cleanedName) rather than raw string concatenation
  3. 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

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


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