fix(security): protocol & transport hardening (#27, #30, #31, #34)#41
Conversation
Four scattered Low/Info findings; #32 closed as spec-compliant (no change). Each passed a PoC-backed adversarial red-team pass (AGENTS.md §2) that caught two real residuals, folded in. Full suite 648 green; warnings-as-errors clean. - #30 OutOfBand.ReadWebRedirect validates the peer-supplied redirectUrl: returns null unless an absolute http/https URL with no userinfo and a non-private host, and returns the canonical form. Closes javascript:/data:/file:/user@host passthrough on the "may navigate to" target. (Public-host open redirect remains by design — no allowlist; consumers must confirm.) - #34 OutOfBand.FromPlaintext rejects an absent OR whitespace-only `from` (FormatException FR-OOB-01); build side tightened to ThrowIfNullOrWhiteSpace. (Red-team: IsNullOrEmpty accepted " "/NBSP.) - #31 dispatcher implements FR-THR-04 rule 3: records each ACK request it emits (ThreadState.AckRequested) and consumes the answering pure-ACK rather than re-dispatching it. AckLoopGuard remarks corrected to what's actually enforced; facade-direct ACK requests documented as the app's responsibility. - #27 transport OutboundEndpointPolicy is nullable = inherit DidCommOptions', so the SSRF policy is configured in one place for the pre-send check and both transports' connect pin. Default stays strict (BlockPrivateNetworks=true). Behavior change: a permissive core policy now relaxes the transport guard too (the intended single-source consistency) — documented. - #32 (JWS typ not validated): no change. Parser delegated to DataProofsDotnet.Jose (doesn't surface typ); RFC 7515 typ is advisory, envelope kind is structural — spec-compliant. Closes #27, closes #30, closes #31, closes #34 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
moisesja
left a comment
There was a problem hiding this comment.
Review — PR #41: protocol & transport hardening
Overall this is well-executed. Each fix is surgical, the adversarial red-team pass caught two real residuals (whitespace-only from, non-canonical URL return), and the test coverage is solid. The #32 disposition is properly spec-referenced. A few specific things to address before merging:
- http://2130706433/admin — blocking mechanism is unclear (security)
The test [InlineData("http://2130706433/admin")] passes with Should().BeNull(). The IsSafeRedirectUrl guard is:
if ((parsed.HostNameType is UriHostNameType.IPv4 or UriHostNameType.IPv6)
&& IPAddress.TryParse(parsed.Host, out var ip)
&& OutboundEndpointGuard.IsPrivateOrReserved(ip))
On .NET, System.Uri may classify a decimal-integer host (2130706433) as UriHostNameType.Dns, not IPv4. If so, the guard never enters the block — and IPAddress.TryParse("2130706433", ...) would return false anyway (dotted-decimal only). The test could be green because Uri.TryCreate outright rejects the decimal form on .NET 10, not because IsPrivateOrReserved fires.
Action needed: add a comment that pins the rejection mechanism. A future refactor to a more lenient URI parser could silently re-open this hole if nobody knows which layer is doing the blocking.
-
No HTTP transport inheritance test (#27)
SendAsync_inherits_core_OutboundEndpointPolicy_when_transport_policy_unset covers the WS path. The HTTP path in HttpDidCommBuilderExtensions uses the same inherit-when-null pattern via sp.GetService<IOptions>()?.Value... but has no equivalent test. A minimal IServiceCollection-based integration test proving the core policy reaches the HTTP ConnectCallback's OutboundEndpointGuard would close this gap. -
AckRequested stuck-true if the expected ACK never arrives (#31)
ThreadState.AckRequested is cleared only when the answering pure-ACK arrives. If the peer never sends it (network loss, peer bug), the flag stays true for the thread's LRU lifetime. A later different pure-ACK on the same thread would be silently dropped as DroppedAsAckLoop. This is bounded by the LRU store (#21) and rule-2 is the real loop barrier, but the AckRequested XML doc should acknowledge it: "If the expected pure-ACK is never received, the flag remains set until eviction; a later unsolicited pure-ACK on the same thread will be incorrectly dropped." -
Open-redirect warning missing from ReadWebRedirect XML doc (#30)
IsSafeRedirectUrl deliberately passes https://evil.example/phishing-page — documented in the CHANGELOG and inline. But the public ReadWebRedirect method itself has no XML warning that the returned URL is still an open redirect target the consumer must validate before navigating. This contract belongs in the method XML doc so it surfaces in IntelliSense — not just in internal comments and the CHANGELOG.
Minor nits (non-blocking)
Issue number references in code comments ((#34), (#30 red-team)) will rot — prefer stable spec section references.
tasks/todo*.md files committed to VCS will accumulate. Consider a tasks/.gitignore.
…direct + ACK-flag caveats (PR #41 review) Addresses the PR #41 review: - #27: add HttpTransportPolicyInheritanceTests — symmetric to the WS inherit test, exercising the real SocketsHttpHandler.ConnectCallback (positive + non-vacuous complement). Closes the untested HTTP inherit path. - #30: add an open-redirect <remarks> warning to OutOfBand.ReadWebRedirect so the consumer-must-confirm contract surfaces in IntelliSense. - #31: document that ThreadState.AckRequested can stay set until LRU eviction if the answering pure-ACK never arrives (benign over-drop; rule-2 is the real loop barrier). - Review item 1 (decimal-IP block "incidental") assessed FALSE — verified System.Uri canonicalizes numeric-encoded IPv4 to dotted form before the guard runs. Added a clarifying comment in IsSafeRedirectUrl pinning that mechanism. Doc-only changes plus 2 new tests; full suite 650 tests green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Addressed the review in c1353ef. Per-item resolution:
Nits: issue-# comment refs kept (they sit alongside FR/spec IDs and aid audit traceability); Out-of-scope observation (not changed here): the HTTP transport surfaces connect-time failures as raw Full suite 650 tests green; build clean (warnings-as-errors). |
Summary
Resolves four scattered Low/Info findings (#27, #30, #31, #34); #32 closed as spec-compliant (no change). Each fix passed a PoC-backed adversarial red-team pass that caught two real residuals (folded in). Full suite: 648 tests green; warnings-as-errors clean (incl. cookbook).
Note: two of these reference code from before the JOSE delegation — handled below.
Fixes
OutOfBand.ReadWebRedirectvalidates the peer-suppliedredirectUrl: returnsnullunless an absolutehttp/httpsURL with no userinfo and a non-private host, and returns the canonical form. Closesjavascript:/data:/file:/user@hostpassthrough.OutOfBand.FromPlaintext(single inbound choke point) rejects an absent or whitespace-onlyfrom(FormatException, FR-OOB-01); build side tightened to match.ThreadState.AckRequested) and consumes the answering pure-ACK instead of re-dispatching it.AckLoopGuarddocs corrected to what's actually enforced.OutboundEndpointPolicyis now nullable (null= inheritDidCommOptions'), so the outbound SSRF policy is configured in one place. Default stays strict (BlockPrivateNetworks=true).DataProofsDotnet.Jose(doesn't surfacetyp); RFC 7515 §4.1.9 makestypadvisory, FR-SIG-04 has no MUST to validate on receive, and envelope kind is structural. Spec-compliant; closing with rationale.Because the transports now inherit the core policy, setting
DidCommOptions.OutboundEndpointPolicy.BlockPrivateNetworks = false(or another permissive core setting) now relaxes the transport connect-time guard as well — previously each transport kept an independent strict default. This is the intended single-source consistency the issue asks for; set an explicit per-transport policy only to diverge deliberately (the facade pre-send always uses the core policy).Adversarial red-team pass (AGENTS.md §2)
Three break-it agents, two real residuals fixed:
AbsoluteUri(strips leading control chars) and reject userinfo (good@evilphishing-display).IsNullOrEmptyaccepted a whitespace-onlyfrom(" "/NBSP) sinceMessage.Validateonly rejects control chars → switched toIsNullOrWhiteSpace.AddDidComm); no NRE. The one behavior change is documented above.Test plan
javascript:/data:/file:/relative/private-IPv4(+decimal)/IPv6-loopback(+mapped)/userinfo →null; valid https → returned; leading-control-char → canonical.from→FormatExceptionFR-OOB-01.Closes #27, closes #30, closes #31, closes #34
🤖 Generated with Claude Code