caddyserver/caddy · error · caddyhttp.Error

invalid request path

Error message

invalid request path

What it means

The FastCGI transport rejects any request whose URL path contains a NUL byte (`\x00`), returning HTTP 400 before dialing the backend. This is a security guard (Caddy issue #4574): PHP backends can be tricked into executing a non-PHP file as PHP code when null bytes survive into SCRIPT_FILENAME/PATH_INFO.

Source

Thrown at modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go:168

// http3 requests have a negative content length for GET and HEAD requests, if that header is not sent.
// see: https://github.com/caddyserver/caddy/issues/6678#issuecomment-2472224182
// Though it appears even if CONTENT_LENGTH is invalid, php-fpm can handle just fine if the body is empty (no Stdin records sent).
// php-fpm will hang if there is any data in the body though, https://github.com/caddyserver/caddy/issues/5420#issuecomment-2415943516

// TODO: better default buffering for fastcgi requests without content length, in theory a value of 1 should be enough, make it bigger anyway
func (t Transport) DefaultBufferSizes() (int64, int64) {
	return 4096, 0
}

// RoundTrip implements http.RoundTripper.
func (t Transport) RoundTrip(r *http.Request) (*http.Response, error) {
	server := r.Context().Value(caddyhttp.ServerCtxKey).(*caddyhttp.Server)

	// Disallow null bytes in the request path, because
	// PHP upstreams may do bad things, like execute a
	// non-PHP file as PHP code. See #4574
	if strings.Contains(r.URL.Path, "\x00") {
		return nil, caddyhttp.Error(http.StatusBadRequest, fmt.Errorf("invalid request path"))
	}

	env, err := t.buildEnv(r)
	if err != nil {
		return nil, fmt.Errorf("building environment: %v", err)
	}

	ctx := r.Context()

	// extract dial information from request (should have been embedded by the reverse proxy)
	network, address := "tcp", r.URL.Host
	if dialInfo, ok := reverseproxy.GetDialInfo(ctx); ok {
		network = dialInfo.Network
		address = dialInfo.Address
	}

	logCreds := server.Logs != nil && server.Logs.ShouldLogCredentials
	loggableReq := caddyhttp.LoggableHTTPRequest{

View on GitHub (pinned to 50e54ee279)

Solutions

  1. No server fix is needed for legitimate traffic: reject/ignore such requests, they are malicious or broken
  2. Ensure the fastcgi transport is only matched by routes you intend (e.g. `php_server` matcher), so static files are served by file_server instead
  3. Keep Caddy updated so the null-byte guard is present in your deployment
  4. Harden the PHP backend (php-cgi) independently, since this guard is defense-in-depth

Example fix

# before: fastcgi transport applied to everything
example.com {
	root * /srv
	reverse_proxy localhost:9000 {
		transport fastcgi
	}
}
# after: only route PHP index requests through fastcgi
example.com {
	root * /srv
	php_server
	file_server
}
Defensive patterns

Strategy: validation

Validate before calling

// server-side: nothing to guard; this is an intentional 400 for malicious paths.
// In tests, assert the behavior:
req := httptest.NewRequest("GET", "/file.png%00.php", nil)
// expect 400 from the fastcgi transport route

Prevention

When it happens

Trigger: A client request with `%00` URL-encoded into the path (e.g. `/uploads/avatar.png%00.php`) reaching a reverse_proxy route using the fastcgi transport. The check is `strings.Contains(r.URL.Path, "\x00")` on the decoded path.

Common situations: Automated scanners probing php-cgi setups; crafted uploads where an attacker appends %00.php to a static file; occasionally broken clients that embed NULs in paths.

Related errors


AI-assisted analysis of caddyserver/caddy@50e54ee279 (2026-08-15). Data as JSON: /api/errors/2445fd118120ff91. Report an issue: GitHub.