joewalnes/websocketd · error

no CGI script named in path %q

Error message

no CGI script named in path %q

What it means

resolveCgiPath cleans the incoming URL path rooted at '/' and rejects it when the result is just '/', meaning no script filename was named. Without this check the handler would have no target file to execute, so it fails the request with this error before any filesystem join happens.

Source

Thrown at libwebsocketd/http.go:197

// resolveCgiPath maps a request URL path to a file inside cgiDir, refusing
// any path that would escape the directory.
//
// req.URL.Path arrives already percent-decoded, and this handler is not
// mounted behind a ServeMux that would normalize it, so "../" segments and
// (on Windows) "..\" segments reach us verbatim. Naively joining such a
// path lets a request name any file on the host, which cgi.Handler would
// then execute — an unauthenticated RCE. We normalize the request path
// ourselves and require the result to stay within cgiDir. checkPathBoundary
// (applied by the caller) additionally defends against symlinks that point
// out of the directory.
func resolveCgiPath(cgiDir, urlPath string) (string, error) {
	// Normalize in slash space, then map to the OS separator. path.Clean
	// collapses "." and ".." lexically; a rooted clean path can never
	// retain a leading "..", so anything that tried to climb out is folded
	// back to the root and lands inside cgiDir.
	clean := path.Clean("/" + filepath.ToSlash(urlPath))
	if clean == "/" {
		return "", fmt.Errorf("no CGI script named in path %q", urlPath)
	}
	filePath := filepath.Join(cgiDir, filepath.FromSlash(clean))

	// Belt and suspenders: confirm the lexical result really is contained.
	// 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)

View on GitHub (pinned to 7a8683dc7f)

Solutions

  1. Request an actual script path, e.g. http://host:8080/script.cgi instead of the root
  2. Fix the client/base URL so the script filename is included in the path
  3. If a root health check is needed, point it at a real endpoint or a non-CGI route

Example fix

// before
fetch('http://host:8080/')
// after
fetch('http://host:8080/handler.cgi')
Defensive patterns

Strategy: validation

Validate before calling

const url = new URL(endpoint)
if (url.pathname === '/' || url.pathname === '') {
	throw new Error('CGI request must name a script path, e.g. /handler.cgi')
}

Type guard

function namesScript(u) {
  const p = new URL(u).pathname.replace(/\/+$/, '')
  return p.length > 0
}

Try / catch

try {
  const res = await fetch(endpoint)
} catch (e) {
  if (e.message.includes('no CGI script named in path')) {
    endpoint = new URL('/handler.cgi', base).toString()
  }
}

Prevention

When it happens

Trigger: GET http://host:8080/ (root) on a CGI-enabled server; a URL that collapses to root after cleaning, like /./ or /../; an empty path portion in the request.

Common situations: Browsers or uptime checks hitting the server root instead of a script endpoint; clients with a misconfigured base URL that omit the script name; link generators producing trailing-only paths.

Related errors


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