@marcel91200 / caddy Mirror · read-only / commits / 1880021e
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

network_proxy: reject proxy URLs that resolve to a port with no host (#7922)

* network_proxy: reject proxy URLs that resolve to a port with no host The host check after placeholder replacement had its arguments swapped: strings.Split("", pUrl.Host)[0] == ":" This splits the empty string using pUrl.Host as the separator, so it returns [""] and element 0 is always "", never ":". The comparison was dead for every possible host value, which meant the "http://:80" case named in the comment directly below it was never rejected. Such a URL was handed back as the proxy, and the failure surfaced later as a confusing dial error instead of the intended message. Only the pUrl.Host == "" half of the condition ever did anything, so "/some/path" was still caught. Use url.URL.Hostname(), which returns the host with any port stripped and is "" for exactly the two cases the comment describes -- ":80" and "" -- while leaving IPv6 literals such as "[::1]:80" and userinfo forms intact. That collapses both clauses into one expression. Also adds tests for this function; the package previously had none. * network_proxy: cover userinfo-only and IPv6 hosts in the tests Differential-tested the old predicate against the new one across 36 URL forms. Every behavioural change is in the same direction -- previously accepted, now rejected -- and all of them are host-less. Nothing that was rejected before is accepted now, and no legitimate host form changes. Two of those forms were worth pinning down in the test: - "http://user:pass@:8080" parses with Host ":8080", so it is just as host-less as "http://:80". The comment in the source doesn't name this variant, but it was accepted before and is rejected now. - IPv6 literals are full of colons, so a repair that split Host on ":" rather than using Hostname() could plausibly reject them. Added "[::1]:8080" and "[2001:db8::1]" as regression guards; both are accepted before and after, which is the point. On unfixed master the port-only and userinfo cases both fail; the IPv6 cases pass on both sides.

Rohit Behera committed Aug 10, 2026 at 11:28 UTC 1880021e76b8b0067fe54c5e55ccd4647c85d637
2 files changed +115 -1
modules/internal/network/networkproxy.go
+1 -1
@@ -78,7 +78,7 @@ func (p ProxyFromURL) ProxyFunc() func(*http.Request) (*url.URL, error) {
78 if err != nil {
79 p.logger.Warn("failed to derive transport proxy from network_proxy URL")
80 pUrl = nil
81 - } else if pUrl.Host == "" || strings.Split("", pUrl.Host)[0] == ":" {
81 + } else if pUrl.Hostname() == "" {
82 // url.Parse does not return an error on these values:
83 //
84 // - http://:80
modules/internal/network/networkproxy_test.go new
+114
@@ -0,0 +1,114 @@
1 +package network
2 +
3 +import (
4 + "context"
5 + "net/http"
6 + "net/http/httptest"
7 + "testing"
8 +
9 + "go.uber.org/zap"
10 +
11 + "github.com/caddyserver/caddy/v2"
12 +)
13 +
14 +// TestProxyFromURLPlaceholderHostValidation checks that a network_proxy URL
15 +// which resolves to a value without a host is rejected after placeholders are
16 +// applied. url.Parse accepts both "http://:80" and "/some/path" without
17 +// returning an error, so ProxyFunc has to reject them itself.
18 +func TestProxyFromURLPlaceholderHostValidation(t *testing.T) {
19 + for i, tc := range []struct {
20 + name string
21 + url string
22 + hostRepl string
23 + wantHost string
24 + wantErr bool
25 + }{
26 + {
27 + name: "port only, no host",
28 + url: "http://{proxy.host}:80",
29 + hostRepl: "",
30 + wantErr: true,
31 + },
32 + {
33 + name: "no scheme or host, path only",
34 + url: "{proxy.host}/some/path",
35 + hostRepl: "",
36 + wantErr: true,
37 + },
38 + {
39 + // url.Parse puts the userinfo elsewhere, so Host is still just
40 + // ":8080" here. The comment above the check doesn't name this
41 + // form, but it is equally host-less and equally unusable.
42 + name: "userinfo but no host",
43 + url: "http://user:pass@{proxy.host}:8080",
44 + hostRepl: "",
45 + wantErr: true,
46 + },
47 + {
48 + name: "host and port",
49 + url: "http://{proxy.host}:8080",
50 + hostRepl: "proxy.example.com",
51 + wantHost: "proxy.example.com",
52 + wantErr: false,
53 + },
54 + {
55 + name: "host without port",
56 + url: "http://{proxy.host}",
57 + hostRepl: "proxy.example.com",
58 + wantHost: "proxy.example.com",
59 + wantErr: false,
60 + },
61 + {
62 + // Guards against a repair that splits Host on ":" instead of
63 + // using Hostname(): an IPv6 literal is full of colons and must
64 + // not be mistaken for a missing host.
65 + name: "IPv6 literal with port",
66 + url: "http://[{proxy.host}]:8080",
67 + hostRepl: "::1",
68 + wantHost: "::1",
69 + wantErr: false,
70 + },
71 + {
72 + name: "IPv6 literal without port",
73 + url: "http://[{proxy.host}]",
74 + hostRepl: "2001:db8::1",
75 + wantHost: "2001:db8::1",
76 + wantErr: false,
77 + },
78 + } {
79 + p := ProxyFromURL{URL: tc.url, logger: zap.NewNop()}
80 +
81 + repl := caddy.NewReplacer()
82 + repl.Set("proxy.host", tc.hostRepl)
83 +
84 + req := httptest.NewRequest(http.MethodGet, "http://example.com/", nil)
85 + req = req.WithContext(context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl))
86 +
87 + proxyURL, err := p.ProxyFunc()(req)
88 +
89 + if tc.wantErr {
90 + if err == nil {
91 + t.Errorf("Test %d (%s): expected an error for %q but got none (proxy URL: %v)",
92 + i, tc.name, tc.url, proxyURL)
93 + }
94 + if proxyURL != nil {
95 + t.Errorf("Test %d (%s): expected a nil proxy URL for %q, got %v",
96 + i, tc.name, tc.url, proxyURL)
97 + }
98 + continue
99 + }
100 +
101 + if err != nil {
102 + t.Errorf("Test %d (%s): unexpected error for %q: %v", i, tc.name, tc.url, err)
103 + continue
104 + }
105 + if proxyURL == nil {
106 + t.Errorf("Test %d (%s): expected a proxy URL for %q, got nil", i, tc.name, tc.url)
107 + continue
108 + }
109 + if proxyURL.Hostname() != tc.wantHost {
110 + t.Errorf("Test %d (%s): expected host %q, got %q",
111 + i, tc.name, tc.wantHost, proxyURL.Hostname())
112 + }
113 + }
114 +}