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
+}