diff --git a/code-secure-api/code-secure-api/Api/ApiServer.cs b/code-secure-api/code-secure-api/Api/ApiServer.cs index 2720239..94a9468 100644 --- a/code-secure-api/code-secure-api/Api/ApiServer.cs +++ b/code-secure-api/code-secure-api/Api/ApiServer.cs @@ -1,3 +1,4 @@ +using System.Net; using System.Text.Json.Serialization; using System.Text.RegularExpressions; using CodeSecure.Application; @@ -18,9 +19,16 @@ public static class ApiServer public static void Run(string[] args) { var builder = WebApplication.CreateBuilder(args); + + // Load app configuration to get trusted proxies + var appConfig = AppConfig.Load(); + builder.Services.Configure(options => { - options.ForwardedHeaders = ForwardedHeaders.XForwardedProto | ForwardedHeaders.XForwardedHost; + options.ForwardedHeaders = ForwardedHeaders.XForwardedProto | ForwardedHeaders.XForwardedHost | ForwardedHeaders.XForwardedFor; + + // Parse trusted proxies from configuration + ParseTrustedProxies(appConfig.TrustedProxies, options); }); builder.Host.UseSerilog((context, configuration) => configuration.ReadFrom.Configuration(context.Configuration)); @@ -89,6 +97,56 @@ public static void Run(string[] args) app.LoadAuthenticationProviders(); app.Run(); } + + private static void ParseTrustedProxies(string trustedProxies, ForwardedHeadersOptions options) + { + if (string.IsNullOrWhiteSpace(trustedProxies)) + return; + + var proxies = trustedProxies.Split(',', StringSplitOptions.RemoveEmptyEntries); + + foreach (var proxy in proxies) + { + var trimmedProxy = proxy.Trim(); + + // Check if it's a CIDR block + if (trimmedProxy.Contains('/')) + { + if (TryParseIPNetwork(trimmedProxy, out var network, out var prefixLength)) + { + options.KnownNetworks.Add(new Microsoft.AspNetCore.HttpOverrides.IPNetwork(network, prefixLength)); + } + } + // Single IP address + else if (IPAddress.TryParse(trimmedProxy, out var ipAddress)) + { + options.KnownProxies.Add(ipAddress); + } + } + } + + private static bool TryParseIPNetwork(string cidr, out IPAddress network, out int prefixLength) + { + network = IPAddress.None; + prefixLength = 0; + + var parts = cidr.Split('/'); + if (parts.Length != 2) + return false; + + if (!IPAddress.TryParse(parts[0], out network)) + return false; + + if (!int.TryParse(parts[1], out prefixLength)) + return false; + + // Validate prefix length based on address family + var maxPrefixLength = network.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork ? 32 : 128; + if (prefixLength < 0 || prefixLength > maxPrefixLength) + return false; + + return true; + } } internal sealed partial class SlugifyParameterTransformer : IOutboundParameterTransformer diff --git a/code-secure-api/code-secure-api/Application/AppConfig.cs b/code-secure-api/code-secure-api/Application/AppConfig.cs index 103dc82..958b940 100644 --- a/code-secure-api/code-secure-api/Application/AppConfig.cs +++ b/code-secure-api/code-secure-api/Application/AppConfig.cs @@ -33,6 +33,9 @@ public class AppConfig [Option(Env = "FRONTEND_URL", Default = "")] public string FrontendUrl { get; set; } = string.Empty; + [Option(Env = "TRUSTED_PROXIES", Default = "127.0.0.1,::1")] + public string TrustedProxies { get; set; } = string.Empty; + [JsonIgnore] internal SecurityKey AccessTokenSecurityKey = null!; [JsonIgnore] internal SecurityKey RefreshTokenSecurityKey = null!;