From 05d87b2e0416b7c80bda17110fcd6fd2c5292813 Mon Sep 17 00:00:00 2001 From: Adam Moussa Date: Thu, 20 Aug 2026 12:14:44 -0400 Subject: [PATCH] fix(api): sanitize request path in internal API key logs (SEC-29) Strip CR/LF from Request.Path before logging invalid-key and disallowed-path warnings so CodeQL alerts 4 and 5 close without changing 401/403 behavior. --- .../Middleware/InternalApiKeyMiddleware.cs | 32 +++++++++++++++++-- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/api/src/ProposalSystem.Api/Middleware/InternalApiKeyMiddleware.cs b/api/src/ProposalSystem.Api/Middleware/InternalApiKeyMiddleware.cs index c5b599d..098775b 100644 --- a/api/src/ProposalSystem.Api/Middleware/InternalApiKeyMiddleware.cs +++ b/api/src/ProposalSystem.Api/Middleware/InternalApiKeyMiddleware.cs @@ -14,6 +14,27 @@ public class InternalApiKeyMiddleware "/api/files", ]; +private static string SanitizeForLog(string value) +{ + if (string.IsNullOrEmpty(value)) + { + return string.Empty; + } + + var sanitized = value.Replace("\r", "").Replace("\n", ""); + var builder = new StringBuilder(sanitized.Length); + + foreach (var c in sanitized) + { + if (!char.IsControl(c)) + { + builder.Append(c); + } + } + + return builder.ToString(); +} + private readonly RequestDelegate _next; private readonly byte[] _apiKeyBytes; private readonly ILogger _logger; @@ -32,20 +53,25 @@ public class InternalApiKeyMiddleware context.Request.Headers.TryGetValue("X-Internal-Api-Key", out var providedKey) && !string.IsNullOrEmpty(providedKey.ToString())) { + var path = context.Request.Path.Value ?? ""; + var sanitizedPath = SanitizeForLog(path); + var providedBytes = Encoding.UTF8.GetBytes(providedKey.ToString()); if (!CryptographicOperations.FixedTimeEquals(providedBytes, _apiKeyBytes)) { _logger.LogWarning("Invalid internal API key from {RemoteIp} on {Path}", - context.Connection.RemoteIpAddress, context.Request.Path); + context.Connection.RemoteIpAddress, sanitizedPathForLog); context.Response.StatusCode = 401; return; } - var path = context.Request.Path.Value ?? ""; if (!AllowedPathPrefixes.Any(prefix => path.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))) { + var sanitizedPathForLog = path + .Replace("\r", "") + .Replace("\n", ""); _logger.LogWarning("Internal API key used on disallowed path {Path} from {RemoteIp}", - context.Request.Path, context.Connection.RemoteIpAddress); + sanitizedPathForLog, context.Connection.RemoteIpAddress); context.Response.StatusCode = 403; return; }