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