Skip to content

Commit cc0c0cf

Browse files
nebezfrancislavoie
andauthored
caddyhttp: Security enhancements for client IP parsing (#5805)
Co-authored-by: Francis Lavoie <[email protected]>
1 parent 80acf1b commit cc0c0cf

File tree

3 files changed

+352
-5
lines changed

3 files changed

+352
-5
lines changed

caddyconfig/httpcaddyfile/serveroptions.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ type serverOptions struct {
4646
Protocols []string
4747
StrictSNIHost *bool
4848
TrustedProxiesRaw json.RawMessage
49+
TrustedProxiesStrict int
4950
ClientIPHeaders []string
5051
ShouldLogCredentials bool
5152
Metrics *caddyhttp.Metrics
@@ -217,6 +218,12 @@ func unmarshalCaddyfileServerOptions(d *caddyfile.Dispenser) (any, error) {
217218
)
218219
serverOpts.TrustedProxiesRaw = jsonSource
219220

221+
case "trusted_proxies_strict":
222+
if d.NextArg() {
223+
return nil, d.ArgErr()
224+
}
225+
serverOpts.TrustedProxiesStrict = 1
226+
220227
case "client_ip_headers":
221228
headers := d.RemainingArgs()
222229
for _, header := range headers {
@@ -340,6 +347,7 @@ func applyServerOptions(
340347
server.StrictSNIHost = opts.StrictSNIHost
341348
server.TrustedProxiesRaw = opts.TrustedProxiesRaw
342349
server.ClientIPHeaders = opts.ClientIPHeaders
350+
server.TrustedProxiesStrict = opts.TrustedProxiesStrict
343351
server.Metrics = opts.Metrics
344352
if opts.ShouldLogCredentials {
345353
if server.Logs == nil {

modules/caddyhttp/server.go

Lines changed: 52 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,19 @@ type Server struct {
173173
// remote IP address.
174174
ClientIPHeaders []string `json:"client_ip_headers,omitempty"`
175175

176+
// If greater than zero, enables strict ClientIPHeaders
177+
// (default X-Forwarded-For) parsing. If enabled, the
178+
// ClientIPHeaders will be parsed from right to left, and
179+
// the first value that is both valid and doesn't match the
180+
// trusted proxy list will be used as client IP. If zero,
181+
// the ClientIPHeaders will be parsed from left to right,
182+
// and the first value that is a valid IP address will be
183+
// used as client IP.
184+
//
185+
// This depends on `trusted_proxies` being configured.
186+
// This option is disabled by default.
187+
TrustedProxiesStrict int `json:"trusted_proxies_strict,omitempty"`
188+
176189
// Enables access logging and configures how access logs are handled
177190
// in this server. To minimally enable access logs, simply set this
178191
// to a non-null, empty struct.
@@ -839,17 +852,28 @@ func determineTrustedProxy(r *http.Request, s *Server) (bool, string) {
839852
if s.trustedProxies == nil {
840853
return false, ipAddr.String()
841854
}
842-
for _, ipRange := range s.trustedProxies.GetIPRanges(r) {
843-
if ipRange.Contains(ipAddr) {
844-
// We trust the proxy, so let's try to
845-
// determine the real client IP
846-
return true, trustedRealClientIP(r, s.ClientIPHeaders, ipAddr.String())
855+
856+
if isTrustedClientIP(ipAddr, s.trustedProxies.GetIPRanges(r)) {
857+
if s.TrustedProxiesStrict > 0 {
858+
return true, strictUntrustedClientIp(r, s.ClientIPHeaders, s.trustedProxies.GetIPRanges(r), ipAddr.String())
847859
}
860+
return true, trustedRealClientIP(r, s.ClientIPHeaders, ipAddr.String())
848861
}
849862

850863
return false, ipAddr.String()
851864
}
852865

866+
// isTrustedClientIP returns true if the given IP address is
867+
// in the list of trusted IP ranges.
868+
func isTrustedClientIP(ipAddr netip.Addr, trusted []netip.Prefix) bool {
869+
for _, ipRange := range trusted {
870+
if ipRange.Contains(ipAddr) {
871+
return true
872+
}
873+
}
874+
return false
875+
}
876+
853877
// trustedRealClientIP finds the client IP from the request assuming it is
854878
// from a trusted client. If there is no client IP headers, then the
855879
// direct remote address is returned. If there are client IP headers,
@@ -884,6 +908,29 @@ func trustedRealClientIP(r *http.Request, headers []string, clientIP string) str
884908
return clientIP
885909
}
886910

911+
// strictUntrustedClientIp iterates through the list of client IP headers,
912+
// parses them from right-to-left, and returns the first valid IP address
913+
// that is untrusted. If no valid IP address is found, then the direct
914+
// remote address is returned.
915+
func strictUntrustedClientIp(r *http.Request, headers []string, trusted []netip.Prefix, clientIP string) string {
916+
for _, headerName := range headers {
917+
ips := strings.Split(strings.Join(r.Header.Values(headerName), ","), ",")
918+
919+
for i := len(ips) - 1; i >= 0; i-- {
920+
ip, _, _ := strings.Cut(strings.TrimSpace(ips[i]), "%")
921+
ipAddr, err := netip.ParseAddr(ip)
922+
if err != nil {
923+
continue
924+
}
925+
if !isTrustedClientIP(ipAddr, trusted) {
926+
return ipAddr.String()
927+
}
928+
}
929+
}
930+
931+
return clientIP
932+
}
933+
887934
// cloneURL makes a copy of r.URL and returns a
888935
// new value that doesn't reference the original.
889936
func cloneURL(from, to *url.URL) {

0 commit comments

Comments
 (0)