Repository: caddyserver/caddy (language: go)

Issue:
Allow blocking headers with underscores
Hello!

I've been using Caddy to deploy a Django-based application and I've noticed that all headers are being passed forward through the `reverse_proxy` module. 

When looking at monitoring, I've noticed that bots are able to bypass the caddy's host blocking and smuggle a `Host` header to a Django application by exploiting Django's behaviour of converting headers to uppercase and translating `-` to `_` (so `Host` becomes `HOST_HEADER`).

After researching a bit, I've found [this article](https://github.security.telekom.com/2020/05/smuggling-http-headers-through-reverse-proxies.html) and noticed that I'm able to smuggle headers to the backend like this:
```
>curl -iH "HOST_HEADER:example.com:443" https://my-website.com    
HTTP/2 302 
content-type: text/html; charset=utf-8
date: Wed, 08 Jun 2022 13:17:48 GMT
location: /_util/login/?next=/
referrer-policy: same-origin
server: Caddy
server: gunicorn
vary: Cookie, Origin
x-content-type-options: nosniff
x-frame-options: DENY
content-length: 0
```
This arrives on Django with the spoofed host header.

Meanwhile, when using the normal host header, caddy blocks the request:
```
>curl -iH "HOST:example.com:443" https://my-website.com
HTTP/2 200 
server: Caddy
content-length: 0
date: Wed, 08 Jun 2022 13:17:54 GMT
```

Is there a way to strip all host headers containing `_` underscores? This is a default feature in `nginx`.
This was acknowledged by Django a while ago: https://www.djangoproject.com/weblog/2015/jan/13/security/. 
Allow blocking headers with underscores
Hello!

I've been using Caddy to deploy a Django-based application and I've noticed that all headers are being passed forward through the `reverse_proxy` module. 

When looking at monitoring, I've noticed that bots are able to bypass the caddy's host blocking and smuggle a `Host` header to a Django application by exploiting Django's behaviour of converting headers to uppercase and translating `-` to `_` (so `Host` becomes `HOST_HEADER`).

After researching a bit, I've found [this article](https://github.security.telekom.com/2020/05/smuggling-http-headers-through-reverse-proxies.html) and noticed that I'm able to smuggle headers to the backend like this:
```
>curl -iH "HOST_HEADER:example.com:443" https://my-website.com    
HTTP/2 302 
content-type: text/html; charset=utf-8
date: Wed, 08 Jun 2022 13:17:48 GMT
location: /_util/login/?next=/
referrer-policy: same-origin
server: Caddy
server: gunicorn
vary: Cookie, Origin
x-content-type-options: nosniff
x-frame-options: DENY
content-length: 0
```
This arrives on Django with the spoofed host header.

Meanwhile, when using the normal host header, caddy blocks the request:
```
>curl -iH "HOST:example.com:443" https://my-website.com
HTTP/2 200 
server: Caddy
content-length: 0
date: Wed, 08 Jun 2022 13:17:54 GMT
```

Is there a way to strip all host headers containing `_` underscores? This is a default feature in `nginx`.
This was acknowledged by Django a while ago: https://www.djangoproject.com/weblog/2015/jan/13/security/. 


Discussion / hints:
I've updated the issue description and added more details. 
I'm happy to have a go at this if you'll accept the contribution.
I can probably do this; related to https://github.com/caddyserver/caddy/issues/3913.
I've updated the issue description and added more details. 
I'm happy to have a go at this if you'll accept the contribution.
I can probably do this; related to https://github.com/caddyserver/caddy/issues/3913.

Fix patch:
diff --git a/modules/caddyhttp/headers/headers.go b/modules/caddyhttp/headers/headers.go
index 4cef0a9fbe9..f67df92820f 100644
--- a/modules/caddyhttp/headers/headers.go
+++ b/modules/caddyhttp/headers/headers.go
@@ -118,10 +118,16 @@ type HeaderOps struct {
 	// Sets HTTP headers; replaces existing header fields.
 	Set http.Header `json:"set,omitempty"`
 
-	// Names of HTTP header fields to delete.
+	// Names of HTTP header fields to delete. Basic wildcards are supported:
+	//
+	// - Start with `*` for all field names with the given suffix;
+	// - End with `*` for all field names with the given prefix;
+	// - Start and end with `*` for all field names containing a substring.
 	Delete []string `json:"delete,omitempty"`
 
-	// Performs substring replacements of HTTP headers in-situ.
+	// Performs in-situ substring replacements of HTTP headers.
+	// Keys are the field names on which to perform the associated replacements.
+	// If the field name is `*`, the replacements are performed on all header fields.
 	Replace map[string][]Replacement `json:"replace,omitempty"`
 }
 
@@ -208,7 +214,29 @@ func (ops HeaderOps) ApplyTo(hdr http.Header, repl *caddy.Replacer) {
 
 	// delete
 	for _, fieldName := range ops.Delete {
-		hdr.Del(repl.ReplaceAll(fieldName, ""))
+		fieldName = strings.ToLower(repl.ReplaceAll(fieldName, ""))
+		switch {
+		case strings.HasPrefix(fieldName, "*") && strings.HasSuffix(fieldName, "*"):
+			for existingField := range hdr {
+				if strings.Contains(strings.ToLower(existingField), fieldName[1:len(fieldName)-1]) {
+					delete(hdr, existingField)
+				}
+			}
+		case strings.HasPrefix(fieldName, "*"):
+			for existingField := range hdr {
+				if strings.HasSuffix(strings.ToLower(existingField), fieldName[1:]) {
+					delete(hdr, existingField)
+				}
+			}
+		case strings.HasSuffix(fieldName, "*"):
+			for existingField := range hdr {
+				if strings.HasPrefix(strings.ToLower(existingField), fieldName[:len(fieldName)-1]) {
+					delete(hdr, existingField)
+				}
+			}
+		default:
+			hdr.Del(fieldName)
+		}
 	}
 
 	// replace


Test patch:
diff --git a/modules/caddyhttp/headers/headers_test.go b/modules/caddyhttp/headers/headers_test.go
index fb68225e37e..d74e6fc3a07 100644
--- a/modules/caddyhttp/headers/headers_test.go
+++ b/modules/caddyhttp/headers/headers_test.go
@@ -79,6 +79,26 @@ func TestHandler(t *testing.T) {
 				"Keep-Me": []string{"i swear i'm innocent"},
 			},
 		},
+		{
+			handler: Handler{
+				Request: &HeaderOps{
+					Delete: []string{
+						"*-suffix",
+						"prefix-*",
+						"*_*",
+					},
+				},
+			},
+			reqHeader: http.Header{
+				"Header-Suffix": []string{"lalala"},
+				"Prefix-Test":   []string{"asdf"},
+				"Host_Header":   []string{"silly django... sigh"}, // see issue #4830
+				"Keep-Me":       []string{"foofoofoo"},
+			},
+			expectedReqHeader: http.Header{
+				"Keep-Me": []string{"foofoofoo"},
+			},
+		},
 		{
 			handler: Handler{
 				Request: &HeaderOps{