@marcel91200 / caddy Mirror · read-only / commits / c4d8caa2
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, 01:00 UTC

caddyhttp: reject invalid and dots-only hosts on every protocol (#8328)

Kévin Dunglas committed Nov 3, 2026 at 07:12 UTC c4d8caa2e7dade974f3e8c0e7a351198e0b5a306
2 files changed +67
modules/caddyhttp/server.go
+14
@@ -39,6 +39,7 @@ import (
39 "github.com/quic-go/webtransport-go"
40 "go.uber.org/zap"
41 "go.uber.org/zap/zapcore"
42 + "golang.org/x/net/http/httpguts"
43
44 "github.com/caddyserver/caddy/v2"
45 "github.com/caddyserver/caddy/v2/modules/caddyevents"
@@ -869,6 +870,19 @@ func (s *Server) serveHTTP(w http.ResponseWriter, r *http.Request) error {
870 }
871 }
872
873 + // the host may be used in a file path (`root /srv/{host}`): HTTP/2 and HTTP/3 don't
874 + // validate :authority, and Windows trims trailing dots, so "..." can act like ".."
875 + host, _, err := net.SplitHostPort(r.Host)
876 + if err != nil {
877 + host = r.Host
878 + }
879 + if !httpguts.ValidHostHeader(r.Host) || (host != "" && strings.Trim(host, ".") == "") {
880 + return HandlerError{
881 + Err: errors.New("invalid host"),
882 + StatusCode: http.StatusBadRequest,
883 + }
884 + }
885 +
886 // Drop headers whose names contain `_` or `.`: once FastCGI/CGI/FrankenPHP etc.
887 // rewrites `-` to `_` (and PHP additionally folds `.` to `_` when registering
888 // $_SERVER keys), an underscore or dot alias collides with the legitimate
modules/caddyhttp/server_test.go
+53
@@ -508,6 +508,59 @@ func TestServer_DetermineTrustedProxy_MatchRightMostUntrustedSkippingTrusted(t *
508 assert.Equal(t, clientIP, "45.54.45.54")
509 }
510
511 +func TestServer_serveHTTP_RejectsPathLikeHost(t *testing.T) {
512 + for _, tc := range []struct {
513 + host string
514 + reject bool
515 + }{
516 + {host: ".", reject: true},
517 + {host: "..", reject: true},
518 + {host: "..:443", reject: true},
519 + {host: "[..]:443", reject: true},
520 + {host: "../../..", reject: true},
521 + {host: "a/b", reject: true},
522 + {host: `a\b`, reject: true},
523 + {host: `..\..`, reject: true},
524 + {host: "a\x00b", reject: true},
525 + {host: "...", reject: true},
526 + {host: "....:443", reject: true},
527 + {host: ".. ", reject: true},
528 + {host: "a b", reject: true},
529 + {host: "example.com", reject: false},
530 + {host: "example.com:8443", reject: false},
531 + {host: "example.com.", reject: false},
532 + {host: "..example.com", reject: false},
533 + {host: "127.0.0.1:80", reject: false},
534 + {host: "[::1]:443", reject: false},
535 + {host: "[::1]", reject: false},
536 + } {
537 + t.Run(tc.host, func(t *testing.T) {
538 + called := false
539 + s := &Server{
540 + logger: zap.NewNop(),
541 + primaryHandlerChain: HandlerFunc(func(http.ResponseWriter, *http.Request) error {
542 + called = true
543 + return nil
544 + }),
545 + }
546 +
547 + req := httptest.NewRequest(http.MethodGet, "/", nil)
548 + req.Host = tc.host
549 +
550 + err := s.serveHTTP(httptest.NewRecorder(), req)
551 + if !tc.reject {
552 + require.NoError(t, err)
553 + assert.True(t, called)
554 + return
555 + }
556 + var handlerErr HandlerError
557 + require.ErrorAs(t, err, &handlerErr)
558 + assert.Equal(t, http.StatusBadRequest, handlerErr.StatusCode)
559 + assert.False(t, called)
560 + })
561 + }
562 +}
563 +
564 // TestServer_serveHTTP_DropsUnderscoreHeader covers GHSA-f59h-q822-g45g: an
565 // underscore-named alias (e.g. `Remote_user`) of a hyphenated header must be
566 // dropped before any handler runs.