fix(security): trusted-proxy ForwardedHeaders so LoginThrottle keys on the real client IP (plan R2-07 T2)
This commit is contained in:
@@ -337,6 +337,18 @@ try
|
||||
}
|
||||
|
||||
// Middleware pipeline
|
||||
|
||||
// Trusted-proxy forwarded headers (arch-review R2 N2): behind Traefik every client
|
||||
// shares the proxy's IP, collapsing the LoginThrottle's {username}|{ip} keys onto
|
||||
// one address (per-IP isolation gone + cheap username-lockout DoS). When enabled,
|
||||
// X-Forwarded-For from the CONFIGURED proxies only is honored. MUST run first —
|
||||
// everything downstream keys on Connection.RemoteIpAddress.
|
||||
var forwardedOptions = builder.Configuration
|
||||
.GetSection(ZB.MOM.WW.ScadaBridge.Security.ForwardedHeadersSecurityOptions.SectionName)
|
||||
.Get<ZB.MOM.WW.ScadaBridge.Security.ForwardedHeadersSecurityOptions>() ?? new();
|
||||
if (ZB.MOM.WW.ScadaBridge.Security.ForwardedHeadersSetup.Build(forwardedOptions) is { } fho)
|
||||
app.UseForwardedHeaders(fho);
|
||||
|
||||
app.UseWebSockets();
|
||||
app.UseRouting();
|
||||
app.UseAuthentication();
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.HttpOverrides;
|
||||
using System.Net;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.Security;
|
||||
|
||||
/// <summary>
|
||||
/// Config shape for trusted-proxy forwarded-header handling
|
||||
/// (<c>ScadaBridge:Security:ForwardedHeaders</c>). Disabled by default: a deployment
|
||||
/// with no reverse proxy must NOT honor X-Forwarded-For (a client could spoof its
|
||||
/// throttle key). The documented Traefik topologies enable it with the docker
|
||||
/// network range so LoginThrottle keys on the REAL client IP (arch-review R2 N2).
|
||||
/// </summary>
|
||||
public sealed class ForwardedHeadersSecurityOptions
|
||||
{
|
||||
/// <summary>Configuration section binding this options class.</summary>
|
||||
public const string SectionName = "ScadaBridge:Security:ForwardedHeaders";
|
||||
|
||||
/// <summary>Whether X-Forwarded-For from trusted proxies is honored.</summary>
|
||||
public bool Enabled { get; set; }
|
||||
|
||||
/// <summary>Exact proxy IPs allowed to supply X-Forwarded-For.</summary>
|
||||
public string[] KnownProxies { get; set; } = [];
|
||||
|
||||
/// <summary>CIDR networks allowed to supply X-Forwarded-For (e.g. "172.16.0.0/12").</summary>
|
||||
public string[] KnownNetworks { get; set; } = [];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds the <see cref="ForwardedHeadersOptions"/> for <c>app.UseForwardedHeaders</c>.
|
||||
/// Pure and fail-fast: malformed entries throw at startup rather than silently
|
||||
/// trusting nothing/everything. Defaults (loopback) are CLEARED — only the
|
||||
/// explicitly configured proxies/networks are trusted, and ForwardLimit=1 means
|
||||
/// only the entry the trusted proxy itself appended is honored.
|
||||
/// </summary>
|
||||
public static class ForwardedHeadersSetup
|
||||
{
|
||||
/// <summary>
|
||||
/// Builds the forwarded-headers options, or <see langword="null"/> when disabled.
|
||||
/// </summary>
|
||||
/// <param name="options">The bound <see cref="ForwardedHeadersSecurityOptions"/>.</param>
|
||||
/// <returns>The configured <see cref="ForwardedHeadersOptions"/>, or <see langword="null"/> when disabled.</returns>
|
||||
public static ForwardedHeadersOptions? Build(ForwardedHeadersSecurityOptions options)
|
||||
{
|
||||
if (!options.Enabled) return null;
|
||||
var built = new ForwardedHeadersOptions
|
||||
{
|
||||
ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto,
|
||||
ForwardLimit = 1,
|
||||
};
|
||||
built.KnownProxies.Clear();
|
||||
// KnownIPNetworks (System.Net.IPNetwork) is the net10 successor to the now-deprecated
|
||||
// KnownNetworks; UseForwardedHeaders reads it identically.
|
||||
built.KnownIPNetworks.Clear();
|
||||
foreach (var proxy in options.KnownProxies)
|
||||
built.KnownProxies.Add(IPAddress.Parse(proxy));
|
||||
foreach (var network in options.KnownNetworks)
|
||||
{
|
||||
var parts = network.Split('/');
|
||||
built.KnownIPNetworks.Add(new(IPAddress.Parse(parts[0]), int.Parse(parts[1])));
|
||||
}
|
||||
return built;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user