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); } }