fix(router): no-follow-redirect on fan-out/health/discovery clients + bump tmproto to v0.3.2 - #515
Conversation
…ents TMP forbids redirects on provider endpoints (spec §Provider Endpoints). None of the three router-side HTTP clients set CheckRedirect, so a provider — or a provider registry, on the discovery poll — that answers 3xx would replay the signed request body at whatever host DNS/rebind resolved. safeDialContext blocks only private destinations; a public attacker-controlled host would still resolve, so the redirect refusal is the load-bearing guarantee. Add a shared noFollowRedirect helper in safedial.go alongside the existing dial-time SSRF guard and wire it into the default clients constructed by NewRouter, NewHealthChecker, and NewDiscovery. Caller- overridden clients (WithHTTPClient / WithHealthCheckClient / WithDiscoveryClient) remain the caller's responsibility. Tests: * Helper unit test asserts noFollowRedirect returns http.ErrUseLastResponse so net/http surfaces the 3xx to the caller instead of following it. * End-to-end router test: builds a router, points its client at a redirect-emitting httptest server whose Location targets a canary, asserts the router observes the 3xx and the canary receives zero requests. * Wiring tests for HealthChecker and Discovery assert their default clients set CheckRedirect and it returns ErrUseLastResponse when invoked. (Their transports use safeDialContext, which blocks localhost, so an end-to-end variant would need a public target.) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Picks up StripAccess covering signed-URL residue on known assets and credential material carried on unknown asset types (tmproto PR #513), plus the UnknownAsset scrub-path fix that preserves integer precision above 2^53. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The root module's tmproto pin bump left cmd/router, cmd/identity-agent, and e2e/stack out of sync: each pins tmproto directly AND replaces the root adcp-go module to ../.. (or ../../..). MVS resolves against the higher of the two, so the docker builds ran through the containerized CI with "go: updates to go.mod needed" against the shared build cache. Run go mod tidy in each affected sub-module. cmd/context-agent needs no change — it does not replace adcp-go and pulls tmproto indirectly through targeting, which still pins v0.3.0. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Ladon verdict: Approve
Approve.
Clean, well-scoped security fix: no-follow-redirect (http.ErrUseLastResponse) wired into all three default router-side clients (fan-out, health, discovery), with end-to-end test coverage, correct 3xx-as-error handling, and body close on every path. The tmproto v0.3.2 pin is a real release with matching go.sum and conventional-commit markers.
Checked:
- No blocking (critical/high) findings.
- No in-diff medium findings.
high_riskis true only because changed files matchrouter/**; all reasons are(modified)or(added)with no reviewer medium concern, so the modifications are presumed safe (no escalation on the flag alone).gated_pathsis false; author is not on a no-auto-approve team.
One non-blocking follow-up noted by the reviewer: the fourth trust-fetching client (registry sync, router/registry.go:75) lacks the same safeDialContext/CheckRedirect hardening, but it is latent (main.go constructs it with empty URLs). Worth a future pass but not a blocker here.
Decision table: no rows 1–8 fire → row 9 → approve.
There was a problem hiding this comment.
Ladon verdict: Approve
Approve.
Wires a shared noFollowRedirect (returns http.ErrUseLastResponse) into the default fan-out, health, and discovery clients — closing a TMP provider-endpoint redirect-replay gap not covered by safeDialContext (private-only) — plus a consistent tmproto v0.3.0→v0.3.2 pin bump across all workspace modules with matching go.sum hashes.
Checked:
- Default-client-only scoping of the redirect suppression (no broad behavior change).
- End-to-end + wiring coverage via new
router/no_follow_redirect_test.go. - No
adcp/schemas/**oradcp/types_gen.gochanges — schema↔generated-type coherence intact. - No TMP signing/verification, identity-agent TEE, or protocol-managed-skill edits.
- Consistent module resolution across all
go.mod/go.sum.
high_risk is true only because files match router/**; the modifications preserve contracts and the reviewer surfaced no Critical/High/Medium findings. Three manual-sanity checkboxes are unchecked but the primary redirect path is covered by an automated test. No blocking findings, no escalation triggers, no team gate, gated_paths false — falls through to row 9.
Matches the pattern used elsewhere in router/*_test.go (safedial_test.go:84) so the linter's errcheck rule is happy on the new no-follow-redirect regression test. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
b60ba3f
Summary
Closes two related conformance gaps against the TMP provider-endpoint rules and picks up the tmproto artifact-scrub fixes from the just-cut submodule release.
Router-side HTTP clients no longer follow redirects. None of the three router-owned clients (fan-out in
router.go,/healthprobe inhealthcheck.go, discovery poll indiscovery.go) setCheckRedirect. Spec forbids redirects on provider endpoints — a 3xx from a provider (or a provider registry, on the discovery poll) would let net/http replay the signed request body at whatever host DNS/rebind resolved.safeDialContextblocks only private destinations; a public attacker-controlled host would still resolve, so the redirect refusal is the load-bearing guarantee. This PR wires a sharednoFollowRedirecthelper (insafedial.go, alongside the dial-time SSRF guard) into the default clients built byNewRouter,NewHealthChecker, andNewDiscovery. Caller-overridden clients (WithHTTPClient/WithHealthCheckClient/WithDiscoveryClient) remain the caller's responsibility.tmprotopin bumped from v0.3.0 to v0.3.2. Picks up:StripAccessnow clears the URL field on known assets whose access declaredsigned_url(previously left the credential in the query string after nil-ing Access) and scrubsaccess— plusurlwhen the access declaredsigned_url— fromUnknownAssetraw JSON.UnknownAssetscrub path usesjson.Decoder.UseNumberso integers above 2^53 (IDs, epoch-ms timestamps) round-trip verbatim.The router calls
Artifact.StripAccess()on every incoming context-match request, so bumping the pin activates all of the above on the fan-out hop without any local code change on this PR.Test plan
go test ./...intmproto,targeting,router— all greenaccessobject fans out with that object stripped