fix(security): remediate Medium-severity audit cluster (#17–#21)#37
Conversation
Fixes the five Medium-severity findings from the multi-agent security & compliance audit, plus two lower-severity defects on the shared receive code path (#28, #33). Full suite (598 tests) green; warnings-as-errors clean. - #17 EnvelopeReader rejects illegal envelope compositions: enforces the DIDComm v2.1 receive grammar `anoncrypt? authcrypt? sign? plaintext` (auth/anon-flag aware) before content/consistency processing. Adds PRD FR-ENV-04a documenting the receive-acceptance set (broader than the FR-ENV-02 emit set; admits Appendix C.3 anoncrypt(authcrypt(sign))). - #18 NetDidKeyService authorizes a kid only when its id-subject AND controller resolve to the asserted DID (FR-CONSIST-06 / DD-01), walking raw verification methods so `controller` is no longer dropped. - #19 FromPriorValidator normalizes all malformed-JWT parse failures (FormatException/InvalidOperationException/ArgumentException/JsonException/ KeyNotFoundException) to a generic ProtocolException out of UnpackAsync. - #20/#28/#33 ASP.NET Core HTTP receive collapses every failure to one opaque 400 with empty body (reason logged server-side), closing the decryption/recipient-kid oracle, the 500 + dev-stack-trace escape, and the echoed handler-bug detail. 202/415/413 paths unchanged. - #21 InMemoryThreadStateStore is bounded (DefaultMaxEntries=10_000, approximate-LRU), converting an unbounded-growth memory-exhaustion DoS into a few-MB bound. Secure-by-default; no DI wiring change. Closes #17, #18, #19, #20, #21, #28, #33 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
An adversarial red-team pass (one break-it agent per fix, remote-attacker threat model) confirmed #17/#18/#19 hold and surfaced two exploitable flaws in the new #20/#21 code, fixed here, plus two deeper residuals filed as follow-ups (#35 timing oracle, #36 cascade-guard eviction bypass). - #20 callback-overload 500 oracle: the inline-callback overload ran `onReceive` outside the unpack try, so a throwing callback escaped as a 500 — distinguishing "unpacked successfully" from "rejected" and re-exposing an unpack-success oracle. Now wrapped: a post-unpack callback fault is logged and the receive still answers 202 (one-way per FR-TRN-10). - #21 concurrent eviction-storm: GetOrCreate had no mutual exclusion around the over-cap eviction, so N concurrent inserters each ran a full O(n log n) snapshot-sort (measured ~11x redundant work at 16 threads) — a CPU- amplification DoS. Eviction is now single-flight (Interlocked guard); only one pass runs at a time, restoring the intended amortized cost. Tests: + concurrent-eviction-storm regression (bounds eviction passes under 8-thread flood) and + throwing-callback-still-202. Full suite 600 green. Residuals filed, not closed by this PR: #35 (HTTP receive timing side-channel, held-vs-unheld recipient kid — needs a response-time floor), #36 (FR-PROTO-10 cascade guard defeatable by forcing thread-state eviction; see also #29). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Review — strong PR, one issue re-opens the oracle it sets out to closeThis is high-quality work: each fix is narrow, fail-closed, documented with FR/issue refs, and the self-run adversarial pass (callback-500, eviction-storm) is exactly the rigor a security cluster deserves. I traced the envelope grammar gate against the illegal compositions ( One blocking concern before merge, plus two minor notes. 🔴 Blocking —
|
Addresses the blocking review finding: both HTTP receive overloads rethrew EVERY OperationCanceledException assuming a client abort. A downstream DID-resolution timeout surfaces as TaskCanceledException : OCE with the request token NOT cancelled (e.g. net-did's webvh client hits its 100s HttpClient.Timeout against an attacker-controlled host that hangs). That escaped as an unhandled 500 (+ dev stack trace) while a normal undecryptable envelope returns 400 — a clean 400-vs-500 oracle plus a ~100s slowloris hang, re-opening exactly what #20/#33 closed. - Gate all three OCE catches (overload-1 unpack + callback, overload-2 unpack+dispatch) with `when (httpContext.RequestAborted.IsCancellationRequested)` so only a genuine client abort propagates; a downstream-timeout OCE falls through to the uniform 400 (unpack) / logged-202 (callback). - Regression test: a resolver that throws TaskCanceledException with RequestAborted un-cancelled now returns 400, not 500. Minor review notes: - CHANGELOG: call out that handler-bug InvalidOperationException now returns 400 (logged at Error) instead of 500, so operators alert on the log not 5xx. - Soften the InMemoryThreadStateStore LRU doc: "not evicted while actively receiving" holds only within the eviction window (residual #36). Full suite 601 green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Thanks for the catch — the 🔴 Blocking — OCE rethrow gated on the request tokenAll three OCE catches now use
So only a genuine client abort propagates; a downstream-resolution Regression test added — 🟡 Handler-bug 400-vs-500Called out in the CHANGELOG: handler 🟡 LRU doc nitSoftened — the remark now says a thread "is not evicted" only "within the eviction window," and explicitly notes an idle thread can age out under a >(cap − low-water) flood (residual #36). Full suite 601 green. Ready for re-review. |
Summary
Remediates the five Medium-severity findings (#17–#21) from the multi-agent security & compliance audit, plus two lower-severity defects on the same receive code path (#28, #33). Each fix ships with negative + regression tests, and each was then put through an adversarial red-team pass (one break-it agent per fix, remote-attacker threat model) which confirmed #17/#18/#19 hold and surfaced two further hardening items (folded in) and two deeper residuals (filed as #35/#36).
Full suite: 600 tests green (492 Core + 108 Interop);
dotnet buildclean under warnings-as-errors.Fixes
EnvelopeReaderrejects illegal envelope compositions — enforces the DIDComm v2.1 receive grammaranoncrypt? authcrypt? sign? plaintext(auth/anon-flag aware) before any content/consistency processing. Adds PRD FR-ENV-04a documenting the receive-acceptance set (broader than the FR-ENV-02 emit set; admits Appendix C.3anoncrypt(authcrypt(sign))).NetDidKeyServiceauthorizes akidonly when its id-subject andcontrollerresolve to the asserted DID (FR-CONSIST-06 / DD-01), walking raw verification methods socontrolleris no longer dropped. did:key/did:peer keep authorizing.FromPriorValidatornormalizes every malformed-JWT parse failure (FormatException/InvalidOperationException/ArgumentException/JsonException/KeyNotFoundException) to a genericProtocolExceptionout ofUnpackAsync.400with empty body (reason logged server-side) — closes the decryption/recipient-kid oracle, the500+ dev-stack-trace escape, and the echoed handler-bug detail.202/415/413unchanged.InMemoryThreadStateStorebounded (DefaultMaxEntries = 10_000, approximate-LRU) — converts an unbounded-growth memory-exhaustion DoS into a few-MB bound. Secure-by-default.Red-team hardening (folded into this PR)
onReceiveoutside the unpack try, so a throwing callback escaped as a500, distinguishing "unpacked successfully" from "rejected". Now wrapped: a post-unpack callback fault is logged and the receive still answers202(one-way per FR-TRN-10).GetOrCreatehad no mutual exclusion around the over-cap eviction, so N concurrent inserters each ran a full O(n log n) snapshot-sort (~11× redundant work at 16 threads) — a CPU-amplification DoS. Eviction is now single-flight (Interlocked guard).Residuals filed (NOT closed here)
capmessages per suppressed report); best addressed with Problem-report cascade guard: ErrorCount grows without bound for unrepliable (anoncrypt/from-less) reports, never tripping the cap #29.Test plan
MalformedMessageException; positiveauthcrypt(sign)andanoncrypt(authcrypt(sign))round-trips; the spec Appendix C fixtures still pass.iat/ truncated →ProtocolException.400+ empty body; throwing callback →202.Closes #17, closes #18, closes #19, closes #20, closes #21, closes #28, closes #33
🤖 Generated with Claude Code