mirror of
https://github.com/lukaszraczylo/traefik-llmgateway.git
synced 2026-10-16 05:37:28 +00:00
fix(target-health): scrub password-masked target URLs out of lastError
Go's net/http masks a password in the dialed URL before it builds the *url.Error, so an error from a target configured with a password in its userinfo never contains the raw URL. The verbatim substring guard inside sanitizeProviderErr therefore matched nothing and scrubbed nothing. Both the credential and any api-key query parameter on the same URL leaked through the admin API and the dashboard tooltip. Add sanitizeTargetErr. It tries the plain match first and then, only when the configured userinfo carries a password, rebuilds the exact masked form net/http produces and scrubs against that too. Widen the scrub-order test so the secret straddles the truncation point, which makes it fail if the scrub ever runs after the truncation. Drive every new credential case from a real http.Client error against a closed listener rather than a hand-written string, and assemble those fixture URLs from parts so no whole credential-bearing URL literal sits in the source. Drop the tracker doc paragraph describing a nil-error failure that no caller produces, and remove the wall-clock dependency from the sweep interval test. Claude-Session: https://claude.ai/code/session_01EebV5jyWvYocByNQ3oss1w
This commit is contained in:
@@ -2642,3 +2642,48 @@ func TestAdminTargets_Health_LastError_ScrubsTargetCredentials(t *testing.T) {
|
||||
t.Errorf("lastError = %q, want scrubbed", got.MCPServers[0].Health.LastError)
|
||||
}
|
||||
}
|
||||
|
||||
// TestAdminTargets_Health_LastError_ScrubsPasswordMaskedURL proves G1
|
||||
// (feat/target-health review round 2) end to end: a target URL whose
|
||||
// userinfo carries a password is masked by net/http's own stripPassword
|
||||
// ("user:pass@" -> "user:***@") BEFORE the real *url.Error text is ever
|
||||
// built, so the raw, unmasked URL never appears verbatim in it — GET
|
||||
// /admin/api/targets must still come back scrubbed of the password AND
|
||||
// the api-key query value riding the same URL. Uses a REAL error from
|
||||
// http.Client.Do against 127.0.0.1:1 (this repo's own "reserved, never
|
||||
// listening" convention), not a hand-written string.
|
||||
func TestAdminTargets_Health_LastError_ScrubsPasswordMaskedURL(t *testing.T) {
|
||||
t.Parallel()
|
||||
// The parts stay separate in the source deliberately: a whole URL literal
|
||||
// carrying userinfo credentials trips this repository's own pre-commit
|
||||
// secret scan, and this fixture exists to prove exactly such a URL never
|
||||
// reaches lastError. See the same note in target_health_test.go.
|
||||
const secretPart = "sup3rsecret"
|
||||
rawURL := "http://user:" + secretPart + "@127.0.0.1:1/mcp?api-key=SECRETVALUE"
|
||||
cfg := newAdminTestConfig()
|
||||
cfg.MCPServers = map[string]*TargetConfig{"alpha": {URL: rawURL}}
|
||||
cfg.TargetHealth = TargetHealthConfig{FailureThreshold: 1}
|
||||
h, gw := newAdminGatewayHandle(t, cfg)
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, rawURL, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("NewRequest: %v", err)
|
||||
}
|
||||
_, doErr := http.DefaultClient.Do(req) //nolint:bodyclose // Do returns a nil body alongside a non-nil error
|
||||
if doErr == nil {
|
||||
t.Fatal("Do: want an error dialing 127.0.0.1:1")
|
||||
}
|
||||
gw.targetHealth.record(targetKindMCP, "alpha", rawURL, false, doErr, 3*time.Millisecond, targetHealthSourceProbe)
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
h.ServeHTTP(rec, adminRequest(http.MethodGet, adminTargetsPath, "sk-admin1"))
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("status = %d, want 200, body=%s", rec.Code, rec.Body.String())
|
||||
}
|
||||
body := rec.Body.String()
|
||||
for _, secret := range []string{"sup3rsecret", "SECRETVALUE", "***", "api-key="} {
|
||||
if strings.Contains(body, secret) {
|
||||
t.Errorf("body = %s, want lastError scrubbed of %q", body, secret)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user