fix(nuget): harden README fetch with body size limit + redirect host-pinning - #1398
SaurabhMaydeo wants to merge 1 commit into
Conversation
…pinning validateReadme read the package README via io.ReadAll with no size cap, and the shared http.Client had no CheckRedirect. Bound the read with io.LimitReader (5 MiB) and pin every redirect hop to the originating request's host (and, for the real NuGet base, forbid scheme/port downgrades), so an oversized or redirected upstream response can't exhaust validator memory or be steered at an unexpected host. NuGet serves the service index, README, and package index directly without cross-host redirects, so this is behaviour-preserving for real packages. Mirrors the cargo hardening from modelcontextprotocol#1330. Adds tests for README truncation and cross-host redirect refusal.
807b2ee to
d6151d9
Compare
|
Nice, tightly-scoped hardening. Two things I want to call out as genuinely well done before the nits: The redirect pin is anchored correctly. Laundering — hop to a forbidden host and then back to the allowed one — is refused at the forbidden hop, so the forbidden host is never connected to: And re-implementing the 10-hop cap is not redundant — defining Legitimate multi-hop same-host chains still resolve (4 hops, The size cap is enforced before the read, not after. A I also confirmed the behaviour-preservation claim against live nuget.org rather than taking it on faith. The service index resolves both relevant resources to the same host as the base URL: and real README fetches return 200 with no redirect at all ( One note on severity, since the PR description undersells it: this is not CLI-only hardening. Three non-blocking observations: 1. The service-index and package-index reads are still unbounded. The new client is used for all three NuGet calls and the redirect pin covers all three, but only the README path got a size cap. That 209 MiB stays resident for the cache duration. The 2. Truncation is silent and indistinguishable from a genuinely missing token. If a README exceeds the cap and the 3. Host comparison is exact-string, so it over-blocks equivalent hostnames. All four fail closed, so none of these is a hole — but the first two are the same host being refused, which would surface as a confusing publish failure if an upstream ever emitted a differently-cased Worth noting for anyone reading the policy later: the pin anchors to the host of the originating request, and for the README that host comes from the service index's Tests pass, including the pre-existing live-network ones: The new table test covering the scheme/port branch that httptest bases can't reach is a good call — that branch is unreachable from None of the above blocks this. The mechanism does what it says and I couldn't get past either guard. |
JosephDoUrden
left a comment
There was a problem hiding this comment.
Pulled the branch and ran it locally. go build and go test ./internal/validators/... pass, including the live nuget.org tests, and a local merge with current main is clean and still green. I also tried weakening the guards one at a time (removed the LimitReader, made the host check accept everything, dropped the scheme/port branch), every one got caught by the new tests, so the behaviour is properly pinned. The policy matches the merged cargo one from #1330, same 5 MiB cap and same scheme/port rules, just anchored to the request's own host instead of a static allowlist, the description explains why. One small gap vs cargo: the URL built from ReadmeUriTemplate is fetched with no pre-check, only redirect hops are covered. The live index serves the template from api.nuget.org anyway, so pinning it would change nothing for real packages, fine as a follow-up. The earlier comment already covers the service-index read and truncation UX points, so not repeating those. Looks merge-ready to me.
|
Re-checked the current head ( |
Follow-up to #1330, applying the same README-fetch hardening to the NuGet validator. Scoped to NuGet — no behavior change for existing publishers.
NuGet and cargo are the only validators that read a raw README body (npm/pypi decode JSON metadata directly), so this finishes the pair started in #1330.
Changes
io.LimitReader(5 MiB, matchingmaxCargoReadmeBytes), so an oversized response can't exhaust validator memory.nugetRedirectAllowed) with a table test, mirroring cargo'scargoURLAllowed/TestCargoURLAllowed.NuGet has no separate README CDN host like cargo's
static.crates.io: the README URL is built from the service-indexReadmeUriTemplateand served from the same host. So redirects are pinned to the request's own host rather than a static allowlist. If NuGet ever serves READMEs cross-host, this would need to become an allowlist like cargo's.Test plan
go test ./internal/validators/registries/(incl. existing live-API tests)TestNuGetRedirectAllowed(cross-host, http-downgrade, non-default-port, metadata-IP, userinfo) + httptest tests for README truncation at the cap and cross-host redirect refusalgofmt,go vet, andgolangci-lint v2.11.4(the version CI pins) all clean — 0 issuesDocker/Postgres integration tests run in CI.