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, 02:15 UTC
reverseproxy: strip hop-by-hop headers from interim responses (#8256)
Mohamed A. Abdel Fattah committed
Oct 20, 2026 at 10:11 UTC
bfdbeb4a1bf7e267b0b93070ed854b847beef581
2 files changed
+88
-5
modules/caddyhttp/reverseproxy/headers_test.go
+74
@@ -185,3 +185,77 @@ func TestInformationalResponseKeepsHandlerHeaders(t *testing.T) {
185
t.Errorf("Link from the 103 leaked into the final response: %q", got)
186
}
187
}
188
+
189
+// TestInformationalResponseStripsHopByHopHeaders verifies that hop-by-hop
190
+// headers from a 1xx response are not forwarded to the client, including
191
+// headers nominated by the Connection header.
192
+func TestInformationalResponseStripsHopByHopHeaders(t *testing.T) {
193
+ hopByHop := map[string]string{
194
+ "Alt-Svc": `h2="attacker.example:443"; ma=86400`,
195
+ "Keep-Alive": "timeout=999",
196
+ "Proxy-Authenticate": `Basic realm="phish"`,
197
+ "X-Internal": "secret",
198
+ }
199
+
200
+ backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
201
+ w.Header().Set("Link", "</style.css>; rel=preload")
202
+ for k, v := range hopByHop {
203
+ w.Header().Set(k, v)
204
+ }
205
+ w.Header().Set("Connection", "X-Internal")
206
+ w.WriteHeader(http.StatusEarlyHints)
207
+ for k := range hopByHop {
208
+ w.Header().Del(k)
209
+ }
210
+ w.Header().Del("Link")
211
+ w.Header().Del("Connection")
212
+ w.WriteHeader(http.StatusOK)
213
+ }))
214
+ t.Cleanup(backend.Close)
215
+
216
+ h := minimalHandler(0, &Upstream{Host: new(Host), Dial: backend.Listener.Addr().String()})
217
+
218
+ front := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
219
+ _ = h.ServeHTTP(w, prepareTestRequest(r), caddyhttp.HandlerFunc(func(http.ResponseWriter, *http.Request) error {
220
+ return nil
221
+ }))
222
+ }))
223
+ t.Cleanup(front.Close)
224
+
225
+ var earlyHints []textproto.MIMEHeader
226
+ trace := &httptrace.ClientTrace{
227
+ Got1xxResponse: func(code int, header textproto.MIMEHeader) error {
228
+ if code == http.StatusEarlyHints {
229
+ earlyHints = append(earlyHints, header)
230
+ }
231
+ return nil
232
+ },
233
+ }
234
+ req, err := http.NewRequestWithContext(httptrace.WithClientTrace(context.Background(), trace), http.MethodGet, front.URL, nil)
235
+ if err != nil {
236
+ t.Fatal(err)
237
+ }
238
+ resp, err := http.DefaultClient.Do(req)
239
+ if err != nil {
240
+ t.Fatalf("request failed: %v", err)
241
+ }
242
+ resp.Body.Close()
243
+
244
+ if len(earlyHints) != 1 {
245
+ t.Fatalf("103 responses: got %d, want 1", len(earlyHints))
246
+ }
247
+ if got := earlyHints[0].Get("Link"); got != "</style.css>; rel=preload" {
248
+ t.Errorf("Link on the 103: got %q, want %q", got, "</style.css>; rel=preload")
249
+ }
250
+ for k := range hopByHop {
251
+ if got := earlyHints[0].Get(k); got != "" {
252
+ t.Errorf("hop-by-hop header %s on the 103: got %q, want it stripped", k, got)
253
+ }
254
+ }
255
+ if got := earlyHints[0].Get("Connection"); got != "" {
256
+ t.Errorf("Connection on the 103: got %q, want it stripped", got)
257
+ }
258
+ if resp.StatusCode != http.StatusOK {
259
+ t.Fatalf("status: got %d, want %d", resp.StatusCode, http.StatusOK)
260
+ }
261
+}
modules/caddyhttp/reverseproxy/reverseproxy.go
+14
-5
@@ -1211,7 +1211,9 @@ func (h *Handler) reverseProxy(rw http.ResponseWriter, req *http.Request, origRe
1211
}
1212
h := rw.Header()
1213
ownHeaders := h.Clone()
1214
- copyHeader(h, http.Header(header))
1214
+ upstreamHeaders := http.Header(header).Clone()
1215
+ stripHopByHopHeaders(upstreamHeaders)
1216
+ copyHeader(h, upstreamHeaders)
1217
rw.WriteHeader(code)
1218
1219
// Clear headers coming from the backend, keeping our own
@@ -1399,10 +1401,7 @@ func (h *Handler) finalizeResponse(
1401
// and restore it with a canonical Connection header.
1402
upgradeVal := res.Header.Get("Upgrade")
1403
1402
- removeConnectionHeaders(res.Header)
1403
- for _, h := range hopHeaders {
1404
- res.Header.Del(h)
1405
- }
1404
+ stripHopByHopHeaders(res.Header)
1405
1406
if res.StatusCode == http.StatusSwitchingProtocols && upgradeVal != "" {
1407
res.Header.Set("Upgrade", upgradeVal)
@@ -1851,6 +1850,16 @@ func removeConnectionHeaders(h http.Header) {
1850
}
1851
}
1852
1853
+// stripHopByHopHeaders removes hop-by-hop headers from h, including any
1854
+// headers nominated by the Connection header.
1855
+// See RFC 7230, section 6.1.
1856
+func stripHopByHopHeaders(h http.Header) {
1857
+ removeConnectionHeaders(h)
1858
+ for _, hop := range hopHeaders {
1859
+ h.Del(hop)
1860
+ }
1861
+}
1862
+
1863
// statusError returns an error value that has a status code.
1864
func statusError(err error) error {
1865
// if a response-based retry was exhausted, use the actual upstream