From 1de37d07751ffb11fad6aa448d991f824635bed8 Mon Sep 17 00:00:00 2001 From: "dobby-yivi-agent[bot]" <275734547+dobby-yivi-agent[bot]@users.noreply.github.com> Date: Thu, 2 Jul 2026 03:02:54 +0000 Subject: [PATCH] fix: stop leaking raw upstream body in NetworkException (#41) NetworkException embedded the raw upstream HTTP response body in its exception message and exposed it via a public Body property, so any sensitive upstream detail could leak into logs or error surfaces. - Base message is now generic: "HTTP {statusCode} received from upstream service" - Removed the public Body property - The raw body is retained in a private field for diagnostics only The constructor signature is unchanged, so the two call sites (CryptifyClient/PkgClient EnsureSuccessAsync) continue to pass the body, which is now stored privately rather than surfaced. Co-Authored-By: Claude Opus 4.8 --- src/Exceptions/PostGuardException.cs | 10 +++++--- .../NetworkExceptionTests.cs | 25 +++++++++++++------ 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/src/Exceptions/PostGuardException.cs b/src/Exceptions/PostGuardException.cs index 90af8bd..0634203 100644 --- a/src/Exceptions/PostGuardException.cs +++ b/src/Exceptions/PostGuardException.cs @@ -8,16 +8,20 @@ public PostGuardException(string message, Exception inner) : base(message, inner public class NetworkException : PostGuardException { + // Raw upstream response body, retained for diagnostics only. Kept private + // so it is never surfaced in the exception message or via a public property, + // where it could leak sensitive upstream data into logs. See issue #41. + private readonly string _body; + public int StatusCode { get; } - public string Body { get; } public string Url { get; } public NetworkException(int statusCode, string body, string url) - : base($"HTTP {statusCode} at {url}: {body}") + : base($"HTTP {statusCode} received from upstream service") { StatusCode = statusCode; - Body = body; Url = url; + _body = body; } } diff --git a/tests/E4A.PostGuard.Tests/NetworkExceptionTests.cs b/tests/E4A.PostGuard.Tests/NetworkExceptionTests.cs index 2af983a..a48e804 100644 --- a/tests/E4A.PostGuard.Tests/NetworkExceptionTests.cs +++ b/tests/E4A.PostGuard.Tests/NetworkExceptionTests.cs @@ -5,23 +5,34 @@ namespace E4A.PostGuard.Tests; public class NetworkExceptionTests { [Fact] - public void Ctor_PopulatesUrlProperty() + public void Ctor_PopulatesStatusAndUrlProperties() { var ex = new NetworkException(401, "Unauthorized", "https://pkg.postguard.eu/v2/irma/sign/key"); Assert.Equal(401, ex.StatusCode); - Assert.Equal("Unauthorized", ex.Body); Assert.Equal("https://pkg.postguard.eu/v2/irma/sign/key", ex.Url); } [Fact] - public void Ctor_IncludesUrlAndStatusInMessage() + public void Ctor_MessageIsGenericAndIncludesStatus() { var ex = new NetworkException(500, "boom", "https://cryptify.postguard.eu/fileupload/init"); - Assert.Contains("500", ex.Message); - Assert.Contains("https://cryptify.postguard.eu/fileupload/init", ex.Message); - Assert.Contains("boom", ex.Message); + Assert.Equal("HTTP 500 received from upstream service", ex.Message); + } + + [Fact] + public void Ctor_MessageDoesNotLeakRawUpstreamBody() + { + var ex = new NetworkException(500, "sensitive-upstream-detail", "https://cryptify.postguard.eu/fileupload/init"); + + Assert.DoesNotContain("sensitive-upstream-detail", ex.Message); + } + + [Fact] + public void NetworkException_DoesNotExposePublicBodyProperty() + { + Assert.Null(typeof(NetworkException).GetProperty("Body")); } [Fact] @@ -29,7 +40,7 @@ public void Ctor_AllowsEmptyBody() { var ex = new NetworkException(204, "", "https://pkg.postguard.eu/v2/parameters"); - Assert.Equal("", ex.Body); + Assert.Equal(204, ex.StatusCode); Assert.Equal("https://pkg.postguard.eu/v2/parameters", ex.Url); } }