From f48f0a2ea3fc2faf2354941ab4d94002459f0b25 Mon Sep 17 00:00:00 2001 From: Sean <1661003+spbsoluble@users.noreply.github.com> Date: Fri, 21 Aug 2026 10:36:53 -0700 Subject: [PATCH] fix(security): sanitize ApiException in secret retrieval path GetStaticSecret let a failed GetSecretValuesAsync call propagate its raw ApiException, whose SDK-generated Message can echo back request content including the live auth token, into Command's logs via the wrapping AggregateException. Catch it and rethrow a sanitized InvalidSecretConfigurationException, matching the existing pattern in InitClient. Closes #13 --- CHANGELOG.md | 4 ++++ akeyless-pam/AkeylessPam.cs | 13 +++++++++++++ docsource/testing.md | 1 + .../AkeylessPam.Unit.Tests/AkeylessPamTests.cs | 18 ++++++++++++++++++ tests/AkeylessPam.Unit.Tests/README.md | 1 + 5 files changed, 37 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c1802ff..efe5cf8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ - Environment variable overrides are trimmed of leading/trailing whitespace before use. - Hardened validation and logging around connection parameters and authentication failures. +## Fixes + +- **Sanitized `ApiException` in secret retrieval** — a failed Akeyless secret lookup (e.g. HTTP error from `GetSecretValuesAsync`) previously let the raw SDK exception propagate to Keyfactor Command's logs. The SDK's `ApiException.Message` can echo back portions of the request, including the live auth token. Secret retrieval now catches `ApiException`, logs only the HTTP status code, and throws a sanitized `InvalidSecretConfigurationException`, matching the existing pattern in authentication (`InitClient`). + # v1.0.0 Initial release of the Akeyless PAM Provider for Keyfactor Command and Universal Orchestrator. diff --git a/akeyless-pam/AkeylessPam.cs b/akeyless-pam/AkeylessPam.cs index dc4d97d..0fcfebe 100644 --- a/akeyless-pam/AkeylessPam.cs +++ b/akeyless-pam/AkeylessPam.cs @@ -419,6 +419,19 @@ private async Task GetStaticSecret(IAkeylessApiClient client, AkeylessCo configurationInfo.SecretName, configurationInfo.SecretType); return result; } + catch (ApiException ex) + { + // NOTE: the exception object itself (not just ex.Message) is intentionally excluded from + // the log call — see the matching comment in InitClient. ApiException error content may + // echo back portions of the request, including the live auth token, and most ILogger + // providers render an attached exception's Message/ToString() regardless of the message + // template, so passing `ex` here would defeat that exclusion. + Logger.LogError( + "Akeyless API exception while retrieving secret '{SecretName}' (HTTP {StatusCode})", + configurationInfo.SecretName, ex.ErrorCode); + throw new InvalidSecretConfigurationException( + $"Unable to retrieve secret '{configurationInfo.SecretName}' from Akeyless (HTTP {ex.ErrorCode})."); + } finally { Logger.MethodExit(); diff --git a/docsource/testing.md b/docsource/testing.md index 2e8a585..8820443 100644 --- a/docsource/testing.md +++ b/docsource/testing.md @@ -81,6 +81,7 @@ Integration tests load credentials from environment variables. As a convenience | `GetPassword_StaticJson_MissingField_ThrowsInvalidSecretConfigurationException` | JSON content, requested field absent → exception | | `GetPassword_SecretNotInResponse_ThrowsInvalidSecretConfigurationException` | API response does not contain the requested secret name → exception | | `GetPassword_EmptySecretValue_ThrowsInvalidSecretConfigurationException` | Secret found but value is empty → exception | +| `GetPassword_GetSecretValuesThrowsApiException_WrapsAsInvalidSecretConfigurationException` | `GetSecretValuesAsync` throws `ApiException` → sanitized `InvalidSecretConfigurationException` (message excludes the raw SDK exception content) | #### Configuration Model (`AkeylessConfigurationTests`) diff --git a/tests/AkeylessPam.Unit.Tests/AkeylessPamTests.cs b/tests/AkeylessPam.Unit.Tests/AkeylessPamTests.cs index 58da6be..35408d3 100644 --- a/tests/AkeylessPam.Unit.Tests/AkeylessPamTests.cs +++ b/tests/AkeylessPam.Unit.Tests/AkeylessPamTests.cs @@ -613,6 +613,24 @@ public void GetPassword_StaticText_PlainString_ReturnsAsIs() Assert.Equal("my-password", result); } + [Fact] + public void GetPassword_GetSecretValuesThrowsApiException_WrapsAsInvalidSecretConfigurationException() + { + var mock = new Mock(); + mock.Setup(c => c.Authenticate(It.IsAny(), It.IsAny())) + .Returns("fake-token"); + mock.Setup(c => c.GetSecretValuesAsync(It.IsAny>(), It.IsAny())) + .Throws(new ApiException(404, "Not Found: request body echo including fake-token")); + + var pam = new AkeylessPam(_ => mock.Object); + + var ex = Assert.Throws(() => + pam.GetPassword(Params.Instance(), Params.ValidServer())); + + var inner = Assert.IsType(ex.InnerException); + Assert.DoesNotContain("fake-token", inner.Message); + } + [Fact] public void GetPassword_StaticText_JsonContent_ReturnsFullJsonBlob() { diff --git a/tests/AkeylessPam.Unit.Tests/README.md b/tests/AkeylessPam.Unit.Tests/README.md index 74f41a1..eaf5289 100644 --- a/tests/AkeylessPam.Unit.Tests/README.md +++ b/tests/AkeylessPam.Unit.Tests/README.md @@ -50,6 +50,7 @@ Tests for `AkeylessPam.GetPassword()` covering configuration validation, authent | `GetPassword_StaticJson_MissingField_ThrowsInvalidSecretConfigurationException` | Throws `InvalidSecretConfigurationException` when `StaticSecretFieldName` is not present in the JSON | | `GetPassword_SecretNotInResponse_ThrowsInvalidSecretConfigurationException` | Throws `InvalidSecretConfigurationException` when the API returns an empty dictionary (secret not found) | | `GetPassword_EmptySecretValue_ThrowsInvalidSecretConfigurationException` | Throws `InvalidSecretConfigurationException` when the API returns an empty string for the secret value | +| `GetPassword_GetSecretValuesThrowsApiException_WrapsAsInvalidSecretConfigurationException` | Throws sanitized `InvalidSecretConfigurationException` when `GetSecretValuesAsync` throws `ApiException`; asserts the raw SDK exception content is not present in the resulting message | | `GetPassword_StaticJson_WhitespaceFieldName_ReturnsFullBlob` | A `static_json` secret with a whitespace-only `StaticSecretFieldName` (e.g. a space from the Command UI) returns the full JSON blob | | `GetPassword_StaticKv_JsonStoredAsKv_ParsesViaJson` | When a `static_kv` secret contains JSON instead of `key=value` lines, falls back to JSON parsing |