Harden operator-set target headers (closes #233)
All checks were successful
check / check (push) Successful in 3m8s

Three findings from the review of the per-target request headers
feature.

Configured headers no longer follow a redirect off the origin the
target names. net/http withholds only Authorization and Cookie
across a host change, so an operator's X-Api-Key or PRIVATE-TOKEN
would follow a 302 to a host they never configured. Redirects are
still followed — refusing them would break every destination that
legitimately redirects and would record the 3xx as the delivery's
result — but a hop to another host, another port, or down from
https to http drops every header the target configured. The shared
SSRF-safe transport is kept on that client, so each hop is still
dialled through the private-IP guard.

Trailer joins the reserved names. net/http strips it from the
request it writes, so a configured one was accepted, stored, and
provably never sent.

The invalid-header-name error no longer quotes the text before the
first colon. That text is only a name if it parses as one; when it
does not, a pasted value whose own colon split the line put half a
token into a 400 body. TestParseTargetHeaders_ErrorsNeverQuoteAValue
asserted this invariant while only exercising the after-the-colon
case, and now covers the before-the-colon one.

README documents the http target's config keys, the 300-second
timeout ceiling, the reserved-header list and the redirect
behaviour; the edit form's hint gains Trailer and the redirect note.
This commit is contained in:
2026-08-20 06:11:28 +00:00
parent 3b0ed826bc
commit 2a3d260ee9
9 changed files with 477 additions and 28 deletions

View File

@@ -5,6 +5,7 @@ import (
"log/slog"
"net"
"net/http"
"net/url"
"time"
"go.uber.org/fx"
@@ -167,6 +168,11 @@ func (e *Engine) ExportClientForConfig(
return e.httpTarget.clientForConfig(cfg)
}
// ExportSameDeliveryOrigin exposes sameDeliveryOrigin.
func ExportSameDeliveryOrigin(origin, dest *url.URL) bool {
return sameDeliveryOrigin(origin, dest)
}
// ExportClient returns the http target's shared HTTP client.
func (e *Engine) ExportClient() *http.Client {
return e.httpTarget.client