fix(test): restore the registry SSRF allow-policy after loopback fixtures - #1222
Conversation
…ures The registry SSRF allow-policy (MCP-1076 / CWE-918) is process-global state in internal/registries. Two fixtures disable it so they can serve a loopback httptest registry, and neither restores it: internal/server/mcp_add_from_registry_test.go startTestRegistry internal/server/consistency_official_test.go startOfficialTestRegistry Every later test in the binary then runs with the guard disabled. TestBuildRegistrySourceEntry_RejectsSSRFLiteralIP is the affected case: with the policy left open, buildRegistrySourceEntry accepts https://169.254.169.254/v0.1/servers, the cloud-metadata endpoint the test exists to reject, and all five of its assertions pass without exercising the guard. The suite passes today only because add_registry_source_test.go sorts ahead of both fixtures, so the assertion runs first. -count=2, -shuffle=on, or a new test file sorting between them each make it fail. Restore the default catalog and the allow-policy on cleanup, matching withGuardActive in internal/registries/ssrf_test.go. Production behaviour is unchanged: the registry CRUD paths clone the live config (updatedConfig := *currentConfig), so the flag is carried forward rather than reset. Add TestRegistryFixturesRestoreSSRFPolicy, which resets the policy itself before running each fixture and therefore detects the leak independently of test order.
356c731 to
54f8cf6
Compare
|
Thank you for this, Dawit — merged (auto-merge armed, it lands as soon as the last CI job finishes). This was a real find: the two loopback registry fixtures left the process-global SSRF allow-policy disabled, so TestBuildRegistrySourceEntry_RejectsSSRFLiteralIP only passed because its file happened to sort ahead of them in the run order. I reproduced the -count=2 failure on main, confirmed the LIFO t.Cleanup restore fixes it across the shuffle seeds, and checked that the regression test still fails when the guard itself is removed, so the fix removed the ordering coupling without weakening what the test covers. The writeup made it an easy review — much appreciated. |
|
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
…ail in CI (#1230) * ci: add an advisory go test -shuffle=on lane so order-coupled tests fail in CI The registry SSRF regression test only passed because its file sorted ahead of the two fixtures that flipped the process-global allow-policy (#1222). Nothing in .github/workflows/ ran with -shuffle, so no lane could see a different order. Add 'Unit Tests (shuffle)': the required ubuntu lane's flags and provisioning (skip regex, -race, tscg shim, frontend embed, tiktoken warm-up) plus -shuffle=on, without coverage. It is not in the branch-protection required list, so a newly exposed order coupling in an unrelated package cannot block a PR while the tail of such tests is found. Each package prints its seed as '-test.shuffle <n>'; re-run with -count=1 -shuffle=<n> to reproduce. * fix(test): isolate TestScanForPII_V7FieldViolations from the global BlockedValues Service.Start reaches PopulateBlockedValues, a sync.Once that appends the real hostname and home-dir basename to the package-global BlockedValues and is never undone. Five test files call Start, and this was the one ScanForPII test that did not snapshot the global, so under -shuffle a basename such as "user" tripped rule 2 (blocked_value) on the "terminated by user" payload before rule 7 could report v7_field_invalid. Found by the new shuffle lane's first local run (-shuffle=1788926312892506000). Same save-and-restore the neighbouring ScanForPII tests already use. * fix(review): correct the shuffle-lane replay note about -count=1 -shuffle is not a cacheable go test flag, so a seed replay is never served from the result cache; the note claimed the opposite. Reword to say why -count=1 is still kept and what the seed does and does not reproduce.
Description
Test-only change. No production code is modified and no runtime behaviour changes.
TestBuildRegistrySourceEntry_RejectsSSRFLiteralIP— the MCP-1076 / CWE-918 regression test — can pass without exercising the guard it covers. Reproduction onmain(3.9s, two tests):buildRegistrySourceEntryaccepts the cloud-metadata endpoint. The same command at-count=1passes.Root cause
The allow-policy is process-global (
internal/registries/ssrf.go:42):Its only writer is
SetAllowPrivateRegistryFetch, reached throughSetRegistriesFromConfig(registry_data.go:66). Two fixtures disable the policy so they can serve a loopbackhttptestregistry, and neither restores it:internal/server/mcp_add_from_registry_test.go—startTestRegistryinternal/server/consistency_official_test.go—startOfficialTestRegistryBoth register
t.Cleanup(srv.Close)for the httptest server, but nothing for the global. Every later test in the binary then runs with the policy disabled, so all five assertions inadd_registry_source_test.go:43(metadata endpoint, loopback, two RFC1918 ranges, IPv6 loopback) succeed without reaching the guard.startTestRegistry's doc comment records the assumption behind this — "tests run sequentially so the last writer wins." That holds only for tests that write the policy; tests that read it observe whatever the last fixture left. The comment is corrected here.Why the suite passes today
At
-count=1, Go runs tests in file order, andadd_registry_source_test.gosorts ahead of bothconsistency_official_test.goandmcp_add_from_registry_test.go, so the assertion runs before either fixture.-shuffleis not used in.github/workflows/, so no lane exercises a different order.The order is incidental, not a property anything enforces. On unfixed
main, running the victim alongside the two fixtures under-shufflefails on 2 of 5 seeds at-count=1:-count>1also fails it, as does adoptingt.Parallel(), or adding another policy-disabling fixture in a file sorting ahead ofadd_registry_source_test.go. With this change, all five seeds pass.Production behaviour
Unaffected.
add_registry_source.go:139,remove_registry_source.go:63andedit_registry_source.go:103each doupdatedConfig := *currentConfig, a struct copy that carriesAllowPrivateRegistryFetchforward, so the CRUD paths propagate the operator's setting rather than resetting it. The defect is confined to the test binary.The scope is therefore test reliability, on a CWE-918 guard: a future change that broke
isBlockedIPwould not be caught by the test written to catch it.Fix
t.Cleanup(func() { registries.SetRegistriesFromConfig(nil) })in both fixtures. Anilconfig restores the default catalog and re-enables the policy in one exported call, which also clears the second value leaked from the same line: thetestreg/officialregentry pointing at a closedhttptestURL.TestSetRegistriesFromConfig_NilConfigUsesDefaultsalready pins those semantics.This follows
withGuardActiveininternal/registries/ssrf_test.go:21-31, which does save-and-restore for the same flag within its own package.TestRegistryFixturesRestoreSSRFPolicyis the regression test. It resets the policy itself before running each fixture in a nested subtest, so it detects the leak independently of test order and without-shuffle.Verified failing before the fix (both fixtures,
t.Cleanuplines removed):Not included
-shuffle=onin a CI lane. It would prevent this class from recurring, but it may surface unrelated order dependencies elsewhere in the tree, which should not be resolved as part of this fix. Happy to open it separately.Pre-existing, not addressed here
TestGetServerLogs_MissingFileReturnsEmptyNotErroralso fails under-race -count=2, and it is unrelated to this change: the same command on unmodifiedmainat4e304ba0reproduces both failures.newLogsTestServerregisters a server withAddServerConfigonly, whileNewServerstarts config-as-source-of-truth reconciliation in the background; the server is absent from the config, so the reconciler removes it andGetServerLogscan observe "server not found". It passes 30/30 in isolation under-race, so it needs full-package timing to surface. Same class as this PR, different subsystem — happy to file it separately.Testing
TestRegistryFixturesRestoreSSRFPolicy-count=2FAIL→ok-shuffle=seeds 1-5,-count=1main; 5/5 pass herego test -race -count=2 ./internal/server/..., CI's skip setTestBuildRegistrySourceEntry_RejectsSSRFLiteralIPno longer fails; 0 data races. One unrelated pre-existing failure — see below./scripts/test-api-e2e.shgo vet ./internal/server/gofmtgolangci-lintv2.13.2,.github/.golangci.yml./internal/server/..., byte-identical to cleanmainat4e304ba0(1govetinline hint, 4SA1019deprecated-field uses in existing test files). None in the three files changed here.