Harden operator-set target headers (closes #233)
All checks were successful
check / check (push) Successful in 3m8s
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:
@@ -91,9 +91,10 @@ func TestClientForConfig_TimeoutKeepsSSRFGuard(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestClientForConfig_NoTimeoutUnchanged asserts that with
|
||||
// no per-target timeout the shared SSRF-safe client is
|
||||
// returned unchanged.
|
||||
// TestClientForConfig_NoTimeoutUnchanged asserts that a
|
||||
// config overriding neither the timeout nor the headers gets
|
||||
// the shared SSRF-safe client unchanged: with no configured
|
||||
// headers there is nothing for a redirect policy to strip.
|
||||
func TestClientForConfig_NoTimeoutUnchanged(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -110,3 +111,37 @@ func TestClientForConfig_NoTimeoutUnchanged(t *testing.T) {
|
||||
"must be returned unchanged",
|
||||
)
|
||||
}
|
||||
|
||||
// TestClientForConfig_HeadersKeepSSRFGuard asserts that the
|
||||
// redirect policy a target's configured headers install is
|
||||
// added to a client that still carries the SSRF-safe
|
||||
// transport. The guard is a dial hook, so keeping it is what
|
||||
// makes each redirect hop pass the private-IP check too.
|
||||
func TestClientForConfig_HeadersKeepSSRFGuard(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
engine := newSSRFTestEngine()
|
||||
|
||||
cfg := &delivery.HTTPTargetConfig{
|
||||
URL: "https://example.com/with-headers",
|
||||
Headers: map[string]string{
|
||||
"X-Api-Key": "configured",
|
||||
},
|
||||
}
|
||||
|
||||
client := engine.ExportClientForConfig(cfg)
|
||||
|
||||
require.NotNil(t, client.CheckRedirect,
|
||||
"configured headers must install a redirect policy",
|
||||
)
|
||||
|
||||
assert.Same(t,
|
||||
engine.ExportClient().Transport, client.Transport,
|
||||
"the SSRF-safe transport must be reused, not dropped",
|
||||
)
|
||||
|
||||
assert.Equal(t,
|
||||
engine.ExportClient().Timeout, client.Timeout,
|
||||
"the shared client's timeout must be inherited",
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user