test(sec-4045): regression gates for the wake SSRF, refusal oracle, register counter, DNS guard and snapshot mode - #30
Merged
Conversation
…er counter Four regression gates for the SEC-4045 PoC steps that the merge of PRs #25-#29 left ungated at the dispatch level: - a stored Web Push record naming an internal service is provisioned and woken through the router with a real WebPushSender; the wake fails and a live loopback listener accepts nothing. This is the test PG-1's remediation plan asked for by name, one layer above sender::tests::production_policy_never_dials_loopback. - all four poc-harness fixtures (open port, 404 path, closed port, redirector) are refused with one identical reason and nothing about the target comes back, so the difference between refusals is no longer a port-scanning oracle. - the two register refusals no existing gate scraped -- no sender for the platform, and the per-token cap -- leave gateway_register_total alone. That counter is what the PoC quoted as proof the flood landed. - DEFAULT_METRICS_BIND is a loopback address, so moving the counters onto their own router cannot be undone by a change to its default. Both socket-using gates bind loopback only and assert on a listener that accepted nothing, so reverting a fix to watch them go red cannot send traffic off the machine. Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
…mode lifecycle Two more gaps the merge of PRs #25-#26 left open: - egress: guarded_resolver_refuses_loopback_names exercises the resolver on its own, but nothing checked that hardened_client_builder installs it, that the refusal is the resolver's rather than an incidental TLS or connection-refused failure, or that it lands before the dial. A live loopback listener is the only witness that can tell "refused" from "tried and failed", so the gate binds one and asserts it accepted nothing. localhost is the whole vector set deliberately: this is the one that opens a socket, so a revert must dial 127.0.0.1 and nothing else -- the link-local and private-range hosts stay in is_public_ip_table, which performs no I/O. - store: the snapshot mode was gated at 0644 on open and at 0600 on write, but not across the lifecycle, and not for the other ways an older build could have left it loose. The new gate walks a loose file of each shape through open -> tighten -> reload -> mutate -> rewrite, and asserts the raw token is really in the file, so the mode is protecting something rather than an empty fixture. A fix that tightened on open but let the rewrite path fall back to the umask default would pass the existing gates and fail this one. Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
…scheme `wake_to_a_stored_internal_endpoint_never_dials` used an `http://` endpoint, and passed for the wrong reason. The hardened client sets `https_only(!policy.allows_loopback())`, so reqwest refused that URL client-side before `validate_webpush_endpoint` was ever consulted. Proved by reverting the endpoint policy to accept anything parseable: the gate stayed green. As written it pinned the scheme check twice and the SSRF guard not at all — it would have stayed green through a regression that let internal hosts back into the policy, which is the finding it is named for. Over `https` the scheme check does not apply and the endpoint policy is the only thing between the wake and the listener. Same revert now fails with `left: 1, right: 0` — one accepted connection — in 5.3s, against a loopback listener the test owns, so watching it go red still sends nothing off the machine. Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Six regression gates for the SEC-4045 PoC steps that PRs #25–#29 fixed but left ungated, plus one fix to a gate that passed for the wrong reason. Tests only — no source behaviour changes.
Gap analysis first
Those PRs already added substantial coverage (
tests/api.rs,src/store.rsandsrc/egress.rsunit tests), so this adds only what was missing:sender::tests::production_policy_never_dials_loopback,egress::tests::webpush_endpoint_rejectionswake_to_a_stored_internal_endpoint_never_dialsregister_refusals_reveal_nothing_about_the_targetgateway_register_totalrefused_registrations_are_never_counted/metricsoff the public routermetrics_is_not_served_on_the_public_routerdefault_metrics_bind_is_loopback(pins the default the move relies on)guarded_resolver_refuses_loopback_nameshardened_client_refuses_a_name_that_resolves_to_loopbackevery_loose_snapshot_mode_is_tightened_for_the_whole_lifecycleA gate that passed for the wrong reason
wake_to_a_stored_internal_endpoint_never_dialsoriginally used anhttp://endpoint. The hardened client setshttps_only(!policy.allows_loopback()), so reqwest refused that URL client-side beforevalidate_webpush_endpointwas consulted at all. Reverting the endpoint policy to accept anything parseable left the gate green — it was pinning the scheme check twice and the SSRF guard not at all, and would have survived exactly the regression it is named for.Switched to
https, where the scheme check does not apply and the endpoint policy is the only thing between the wake and the listener. The same revert now fails.This is why each gate below was checked by reverting its fix rather than by reading it.
Red-then-green evidence
Every revert was restored in the same shell invocation and the restore proven with
git diffbefore anything else ran.every_loose_snapshot_mode_is_tightened_for_the_whole_lifecycle— revert:secretfile::tighten_to_owner_only(&path)removed fromStore::open_with_limits.(388 = 0o604, 384 = 0o600.) 1.7s.
hardened_client_refuses_a_name_that_resolves_to_loopback— revert:GuardedResolver::resolvereturns every resolved address unfiltered.5.0s.
wake_to_a_stored_internal_endpoint_never_dials(after the fix above) — revert:validate_webpush_endpointaccepts anything parseable.One connection reached the listener. 5.3s.
Hermetic when red, not only when green
Every socket-using gate binds loopback only and asserts on a listener it owns that accepted nothing. Reverting a fix to watch one go red cannot send traffic off the machine — the slowest red run above is 5.3s against 127.0.0.1. The link-local, CGNAT and private-range vectors live in
is_public_ip_table, which performs no I/O at all.This matters because the first cut of the equivalent tests in
vti-didcomm-jswas hermetic only while passing: reverted, its unroutable vectors dialed the real network for 181–226s per file.Two repo-specific traps these avoid
/metricsis on the management router, so the gates build the public and management routers over one sharedAppState, as the existing tests do.flush()first.Verification
cargo fmt --all --checkclean,cargo clippy --all-targets -- -D warningsclean,cargo test83 passed / 0 failed (59 unit + 24 integration).Not gated
Nothing from the PoC harness is left unrepresented, but two of the six gates were not proven by revert in this pass —
register_refusals_reveal_nothing_about_the_targetandrefused_registrations_are_never_counted. They assert on the dispatch-level reason string and the counter respectively, both of which are covered by narrower existing unit tests; I would rather say they are unproven than imply a revert check I did not run.