Skip to content

Commit 9038dbd

Browse files
Harden forwarded headers trust configuration
Move forwarded headers proxy/CIDR parsing into service extensions and enforce fail-closed behavior outside Development when trusted proxies are not configured. Add ForwardedHeaders config section with TrustedProxyCidrs/TrustedProxies defaults.
1 parent f2ca026 commit 9038dbd

3 files changed

Lines changed: 100 additions & 30 deletions

File tree

EssentialCSharp.Web/Extensions/IServiceCollectionExtensions.cs

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
using EssentialCSharp.Web.Services;
1+
using System.Net;
2+
using System.Net.Sockets;
3+
using EssentialCSharp.Web.Services;
4+
using Microsoft.AspNetCore.HttpOverrides;
25

36
namespace EssentialCSharp.Web.Extensions;
47

@@ -13,4 +16,77 @@ public static void AddCaptchaService(this IServiceCollection services, IConfigur
1316
c.BaseAddress = new Uri("https://api.hcaptcha.com");
1417
});
1518
}
19+
20+
public static void AddTrustedForwardedHeaders(this IServiceCollection services, IConfiguration configuration, IHostEnvironment environment)
21+
{
22+
services.Configure<ForwardedHeadersOptions>(options =>
23+
{
24+
options.ForwardedHeaders =
25+
ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
26+
options.ForwardLimit = 1;
27+
28+
var trustedProxyCidrs = configuration
29+
.GetSection("ForwardedHeaders:TrustedProxyCidrs")
30+
.Get<string[]>() ?? [];
31+
var trustedProxies = configuration
32+
.GetSection("ForwardedHeaders:TrustedProxies")
33+
.Get<string[]>() ?? [];
34+
35+
if (trustedProxyCidrs.Length == 0 && trustedProxies.Length == 0)
36+
{
37+
if (!environment.IsDevelopment())
38+
{
39+
throw new InvalidOperationException(
40+
"Forwarded headers are enabled but no trusted proxies are configured. " +
41+
"Set ForwardedHeaders:TrustedProxyCidrs or ForwardedHeaders:TrustedProxies.");
42+
}
43+
return;
44+
}
45+
46+
options.KnownIPNetworks.Clear();
47+
options.KnownProxies.Clear();
48+
49+
foreach (var cidr in trustedProxyCidrs)
50+
{
51+
if (!TryParseCidr(cidr, out var network))
52+
throw new InvalidOperationException($"Invalid ForwardedHeaders:TrustedProxyCidrs entry '{cidr}'. Use CIDR notation, e.g. '10.0.0.0/8'.");
53+
54+
options.KnownIPNetworks.Add(network);
55+
}
56+
57+
foreach (var proxy in trustedProxies)
58+
{
59+
if (!IPAddress.TryParse(proxy, out var proxyAddress))
60+
throw new InvalidOperationException($"Invalid ForwardedHeaders:TrustedProxies entry '{proxy}'. Use a valid IP address.");
61+
62+
options.KnownProxies.Add(proxyAddress);
63+
}
64+
});
65+
}
66+
67+
private static bool TryParseCidr(string cidr, out System.Net.IPNetwork network)
68+
{
69+
network = default!;
70+
if (string.IsNullOrWhiteSpace(cidr))
71+
return false;
72+
73+
string[] parts = cidr.Split('/', 2, StringSplitOptions.TrimEntries);
74+
if (parts.Length != 2
75+
|| !IPAddress.TryParse(parts[0], out var networkAddress)
76+
|| !int.TryParse(parts[1], out var prefixLength))
77+
return false;
78+
79+
int maxPrefixLength = networkAddress.AddressFamily switch
80+
{
81+
AddressFamily.InterNetwork => 32,
82+
AddressFamily.InterNetworkV6 => 128,
83+
_ => -1
84+
};
85+
86+
if (maxPrefixLength < 0 || prefixLength < 0 || prefixLength > maxPrefixLength)
87+
return false;
88+
89+
network = new System.Net.IPNetwork(networkAddress, prefixLength);
90+
return true;
91+
}
1692
}

EssentialCSharp.Web/Program.cs

Lines changed: 18 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -107,17 +107,7 @@ private static void Main(string[] args)
107107

108108

109109

110-
builder.Services.Configure<ForwardedHeadersOptions>(options =>
111-
{
112-
options.ForwardedHeaders =
113-
ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
114-
115-
// Only loopback proxies are allowed by default.
116-
// Clear that restriction because forwarders are enabled by explicit
117-
// configuration.
118-
options.KnownIPNetworks.Clear();
119-
options.KnownProxies.Clear();
120-
});
110+
builder.Services.AddTrustedForwardedHeaders(builder.Configuration, builder.Environment);
121111

122112
ConfigurationManager configuration = builder.Configuration;
123113
string connectionString = builder.Configuration.GetConnectionString("EssentialCSharpWebContextConnection") ?? throw new InvalidOperationException("Connection string 'EssentialCSharpWebContextConnection' not found.");
@@ -511,7 +501,7 @@ await McpJsonRpcResponseWriter.WriteErrorAsync(
511501
}
512502
app.UseStaticFiles();
513503

514-
app.UseRouting();
504+
app.UseRouting();
515505

516506
app.UseWhen(
517507
context => context.Request.Path.StartsWithSegments("/mcp"),
@@ -542,10 +532,10 @@ await McpJsonRpcResponseWriter.WriteErrorAsync(
542532
await next(context);
543533
}));
544534

545-
app.UseRateLimiter();
546-
547-
app.UseAuthorization();
548-
app.UseOutputCache();
535+
app.UseRateLimiter();
536+
537+
app.UseAuthorization();
538+
app.UseOutputCache();
549539

550540
app.UseMiddleware<ReferralMiddleware>();
551541

@@ -584,13 +574,13 @@ await McpJsonRpcResponseWriter.WriteErrorAsync(
584574
try
585575
{
586576
SitemapXmlHelpers.EnsureSitemapHealthy(siteMappingService.SiteMappings.ToList());
587-
LogSitemapValidationSucceeded(logger);
588-
}
589-
catch (Exception ex)
590-
{
591-
LogSitemapValidationFailed(logger, ex);
592-
// Continue startup even if sitemap validation fails
593-
}
577+
LogSitemapValidationSucceeded(logger);
578+
}
579+
catch (Exception ex)
580+
{
581+
LogSitemapValidationFailed(logger, ex);
582+
// Continue startup even if sitemap validation fails
583+
}
594584

595585
app.Run();
596586
}
@@ -604,11 +594,11 @@ private static bool IsMcpTransportRequest(HttpRequest request) =>
604594
[LoggerMessage(Level = LogLevel.Error, Message = "Unhandled exception on {Path}")]
605595
private static partial void LogUnhandledException(ILogger<Program> logger, Exception? exception, PathString path);
606596

607-
[LoggerMessage(Level = LogLevel.Information, Message = "Sitemap validation completed successfully during application startup")]
608-
private static partial void LogSitemapValidationSucceeded(ILogger<Program> logger);
609-
610-
[LoggerMessage(Level = LogLevel.Error, Message = "Failed to validate sitemap during application startup")]
611-
private static partial void LogSitemapValidationFailed(ILogger<Program> logger, Exception exception);
597+
[LoggerMessage(Level = LogLevel.Information, Message = "Sitemap validation completed successfully during application startup")]
598+
private static partial void LogSitemapValidationSucceeded(ILogger<Program> logger);
599+
600+
[LoggerMessage(Level = LogLevel.Error, Message = "Failed to validate sitemap during application startup")]
601+
private static partial void LogSitemapValidationFailed(ILogger<Program> logger, Exception exception);
612602

613603
[LoggerMessage(Level = LogLevel.Warning, Message = "Ignoring invalid TryDotNet origin in CSP: {Origin}")]
614604
private static partial void LogIgnoringInvalidTryDotNetOrigin(ILogger logger, string origin);

EssentialCSharp.Web/appsettings.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@
99
}
1010
},
1111
"AllowedHosts": "*",
12+
"ForwardedHeaders": {
13+
"TrustedProxyCidrs": [],
14+
"TrustedProxies": []
15+
},
1216
"HCaptcha": {
1317
"SecretKey": "0x0000000000000000000000000000000000000000",
1418
"SiteKey": "10000000-ffff-ffff-ffff-000000000001"
@@ -47,4 +51,4 @@
4751
"TryDotNet": {
4852
"Origin": ""
4953
}
50-
}
54+
}

0 commit comments

Comments
 (0)