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

admin: fix host allow-list comparison to be case-insensitive (#7993)

The admin host check compared r.Host against the allowed URL's Host with byte-for-byte equality. url.Parse does not normalize host case, so an allowed 'http://Example.com:2019' rejected a client Host of 'example.com:2019' with 'host not allowed', and the same happened for an uppercase variant of a lowercase entry like localhost. The Origin check got this treatment in 7973 already, the DNS rebinding Host check right next to it did not. Both read the same allowedOrigins list, so this applies the same strings.EqualFold treatment there. Regression test covers both fold directions, the default localhost entry and the negative case that must keep failing

Abdellatif Anaflous committed Sep 5, 2026 at 13:39 UTC f7d58438b08c1ff613ab2f93b5aaf01c6d692319
2 files changed +86 -1
admin.go
+3 -1
@@ -940,7 +940,9 @@ func (h adminHandler) handleError(w http.ResponseWriter, r *http.Request, err er
940 // rebinding attacks.
941 func (h adminHandler) checkHost(r *http.Request) error {
942 allowed := slices.ContainsFunc(h.allowedOrigins, func(u *url.URL) bool {
943 - return r.Host == u.Host
943 + // Host comparison is case-insensitive per RFC 3986 §3.2.2, same as
944 + // the Origin check below; url.Parse does not normalize host case.
945 + return strings.EqualFold(r.Host, u.Host)
946 })
947 if !allowed {
948 return APIError{
admin_checkhost_case_test.go new
+83
@@ -0,0 +1,83 @@
1 +// Copyright 2015 Matthew Holt and The Caddy Authors
2 +//
3 +// Licensed under the Apache License, Version 2.0 (the "License");
4 +// you may not use this file except in compliance with the License.
5 +// You may obtain a copy of the License at
6 +//
7 +// http://www.apache.org/licenses/LICENSE-2.0
8 +//
9 +// Unless required by applicable law or agreed to in writing, software
10 +// distributed under the License is distributed on an "AS IS" BASIS,
11 +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 +// See the License for the specific language governing permissions and
13 +// limitations under the License.
14 +
15 +package caddy
16 +
17 +import (
18 + "net/http/httptest"
19 + "net/url"
20 + "testing"
21 +)
22 +
23 +// TestAdminHandlerCheckHostCaseInsensitive is the regression test for the
24 +// Host allow-list comparison being case-sensitive. Per RFC 3986 §3.2.2,
25 +// host names are case-insensitive, and url.Parse does not normalize host
26 +// case, so the comparison must fold case. This mirrors the fix applied to
27 +// originAllowed, where an allowed 'http://Example.com:8080' rejected a
28 +// client Host of 'example.com:8080'.
29 +func TestAdminHandlerCheckHostCaseInsensitive(t *testing.T) {
30 + mustParse := func(raw string) *url.URL {
31 + u, err := url.Parse(raw)
32 + if err != nil {
33 + t.Fatalf("invalid URL %q: %v", raw, err)
34 + }
35 + return u
36 + }
37 +
38 + cases := []struct {
39 + name string
40 + allowed *url.URL
41 + host string
42 + wantErr bool
43 + }{
44 + {
45 + name: "allow-list uppercase, request Host lowercase",
46 + allowed: mustParse("http://Example.com:2019"),
47 + host: "example.com:2019",
48 + wantErr: false,
49 + },
50 + {
51 + name: "allow-list lowercase, request Host uppercase",
52 + allowed: mustParse("http://example.com:2019"),
53 + host: "EXAMPLE.com:2019",
54 + wantErr: false,
55 + },
56 + {
57 + name: "default localhost entry, request Host uppercase",
58 + allowed: mustParse("http://localhost:2019"),
59 + host: "LOCALHOST:2019",
60 + wantErr: false,
61 + },
62 + {
63 + name: "different host is still rejected",
64 + allowed: mustParse("http://example.com:2019"),
65 + host: "evil.example.org:2019",
66 + wantErr: true,
67 + },
68 + }
69 +
70 + for _, tc := range cases {
71 + t.Run(tc.name, func(t *testing.T) {
72 + h := adminHandler{allowedOrigins: []*url.URL{tc.allowed}}
73 + r := httptest.NewRequest("GET", "http://"+tc.host+"/config/", nil)
74 + err := h.checkHost(r)
75 + if tc.wantErr && err == nil {
76 + t.Errorf("checkHost(%q) with allowed %v = nil, want error", tc.host, tc.allowed)
77 + }
78 + if !tc.wantErr && err != nil {
79 + t.Errorf("checkHost(%q) with allowed %v = %v, want nil", tc.host, tc.allowed, err)
80 + }
81 + })
82 + }
83 +}