@marcel91200 / caddy Mirror · read-only / commits / 45ba3278
Fork 0

Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS

Mirrored from https://github.com/Slazee/caddy.git

Synced Nov 11, 07:15 UTC

fastcgi: fix HTTPoxy vulnerability (#7934)

* Implement HTTPoxy mitigation in FastCGI Added HTTPoxy mitigation to prevent trusting client-supplied Proxy header for HTTP_PROXY environment variable. * Implement test for HTTPoxy vulnerability protection Add test to ensure HTTPoxy vulnerability is mitigated by dropping client-supplied Proxy headers. * gofmt: format code * revert unrelated gofmt change to replacer_test.go

bzyy1024 committed Aug 19, 2026 at 11:07 UTC 45ba3278b5822f6fa6107a44ca41669b6e722482
2 files changed +57
modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go
+7
@@ -421,6 +421,13 @@ func (t Transport) buildEnv(r *http.Request) (envVars, error) {
421
422 // Add all HTTP headers to env variables
423 for field, val := range r.Header {
424 + // HTTPoxy mitigation: client-supplied Proxy header must not
425 + // be trusted for setting the HTTP_PROXY environment variable.
426 + // See https://httpoxy.org for details.
427 + if http.CanonicalHeaderKey(field) == "Proxy" {
428 + continue
429 + }
430 +
431 header := strings.ToUpper(field)
432 header = headerNameReplacer.Replace(header)
433 env["HTTP_"+header] = strings.Join(val, ", ")
modules/caddyhttp/reverseproxy/fastcgi/fastcgi_test.go
+50
@@ -4,6 +4,7 @@ import (
4 "context"
5 "net"
6 "net/http"
7 + "net/http/httptest"
8 "strings"
9 "testing"
10
@@ -435,3 +436,52 @@ func TestBuildEnvServerAddr(t *testing.T) {
436 }
437 }
438
439 +// TestBuildEnvDropsProxyHeader guards against the HTTPoxy vulnerability
440 +// (CVE-2016-5385): a client-supplied `Proxy` request header must never be
441 +// translated into the HTTP_PROXY environment variable, since many HTTP client
442 +// libraries used by FastCGI backends honor HTTP_PROXY for outbound requests,
443 +// which would let an attacker hijack the backend's traffic.
444 +// See https://httpoxy.org.
445 +func TestBuildEnvDropsProxyHeader(t *testing.T) {
446 + t.Parallel()
447 +
448 + newRequest := func(headers http.Header) *http.Request {
449 + r := httptest.NewRequest(http.MethodGet, "http://example.com/index.php", nil)
450 + // Set the header map directly so non-canonical casing is preserved,
451 + // mimicking a header that reaches buildEnv unmodified.
452 + r.Header = headers
453 + ctx := context.WithValue(r.Context(), caddy.ReplacerCtxKey, caddy.NewReplacer())
454 + ctx = context.WithValue(ctx, caddyhttp.VarsCtxKey, map[string]any{})
455 + ctx = context.WithValue(ctx, caddyhttp.OriginalRequestCtxKey, *r)
456 + return r.WithContext(ctx)
457 + }
458 +
459 + for _, casing := range []string{"Proxy", "proxy", "pRoXy", "PROXY"} {
460 + t.Run("casing "+casing, func(t *testing.T) {
461 + r := newRequest(http.Header{
462 + casing: []string{"http://attacker.example:8080"},
463 + "X-Safe-Hdr": []string{"kept"},
464 + })
465 +
466 + env, err := Transport{}.buildEnv(r)
467 + require.NoError(t, err)
468 +
469 + assert.NotContains(t, env, "HTTP_PROXY", "HTTP_PROXY must never be set from a client header")
470 + // Sanity check that unrelated headers are still forwarded, i.e. the
471 + // filter is narrow and doesn't break normal header passthrough.
472 + assert.Equal(t, "kept", env["HTTP_X_SAFE_HDR"])
473 + })
474 + }
475 +
476 + t.Run("configured HTTP_PROXY is preserved", func(t *testing.T) {
477 + // The filter must only drop the *client* header; an explicitly
478 + // configured env var is trusted and must survive.
479 + r := newRequest(http.Header{"Proxy": []string{"http://attacker.example:8080"}})
480 +
481 + tr := Transport{EnvVars: map[string]string{"HTTP_PROXY": "http://trusted.example:3128"}}
482 + env, err := tr.buildEnv(r)
483 + require.NoError(t, err)
484 +
485 + assert.Equal(t, "http://trusted.example:3128", env["HTTP_PROXY"])
486 + })
487 +}