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.