using Microsoft.Extensions.Options; using System.Net; namespace LittleShop.Middleware; /// /// Middleware to restrict access to admin endpoints based on IP address /// Optional defense-in-depth measure (reverse proxy is preferred approach) /// public class IPWhitelistMiddleware { private readonly RequestDelegate _next; private readonly ILogger _logger; private readonly IPWhitelistOptions _options; public IPWhitelistMiddleware( RequestDelegate next, ILogger logger, IOptions options) { _next = next; _logger = logger; _options = options.Value; } public async Task InvokeAsync(HttpContext context) { // Skip if IP whitelist is disabled if (!_options.Enabled) { await _next(context); return; } var remoteIp = context.Connection.RemoteIpAddress; // Check for X-Forwarded-For header if behind proxy if (_options.UseForwardedHeaders && context.Request.Headers.ContainsKey("X-Forwarded-For")) { var forwardedFor = context.Request.Headers["X-Forwarded-For"].ToString(); var ips = forwardedFor.Split(',', StringSplitOptions.RemoveEmptyEntries); if (ips.Length > 0 && IPAddress.TryParse(ips[0].Trim(), out var parsedIp)) { remoteIp = parsedIp; } } // Check for X-Real-IP header if behind nginx if (_options.UseForwardedHeaders && context.Request.Headers.ContainsKey("X-Real-IP")) { var realIp = context.Request.Headers["X-Real-IP"].ToString(); if (IPAddress.TryParse(realIp, out var parsedIp)) { remoteIp = parsedIp; } } if (remoteIp == null) { _logger.LogWarning("Unable to determine client IP address"); context.Response.StatusCode = 403; await context.Response.WriteAsJsonAsync(new { error = "Access denied", message = "Unable to determine client IP address" }); return; } // Check if IP is whitelisted if (!IsIPWhitelisted(remoteIp)) { _logger.LogWarning("Access denied for IP {IP} to {Path}", remoteIp, context.Request.Path); context.Response.StatusCode = 403; await context.Response.WriteAsJsonAsync(new { error = "Access denied", message = "Admin access is restricted to authorized networks", clientIp = _options.ShowClientIP ? remoteIp.ToString() : "hidden" }); return; } _logger.LogDebug("IP {IP} whitelisted for {Path}", remoteIp, context.Request.Path); await _next(context); } private bool IsIPWhitelisted(IPAddress clientIp) { // Localhost is always allowed if (IPAddress.IsLoopback(clientIp)) { return true; } // Check individual IPs foreach (var allowedIp in _options.WhitelistedIPs) { if (IPAddress.TryParse(allowedIp, out var parsed) && clientIp.Equals(parsed)) { return true; } } // Check CIDR ranges foreach (var cidr in _options.WhitelistedCIDRs) { if (IsInCIDRRange(clientIp, cidr)) { return true; } } return false; } private bool IsInCIDRRange(IPAddress clientIp, string cidr) { try { var parts = cidr.Split('/'); if (parts.Length != 2) { _logger.LogWarning("Invalid CIDR format: {CIDR}", cidr); return false; } var networkAddress = IPAddress.Parse(parts[0]); var prefixLength = int.Parse(parts[1]); // Convert to byte arrays var clientBytes = clientIp.GetAddressBytes(); var networkBytes = networkAddress.GetAddressBytes(); // Check if same address family if (clientBytes.Length != networkBytes.Length) { return false; } // Calculate network mask var maskBytes = new byte[networkBytes.Length]; var fullBytes = prefixLength / 8; var remainingBits = prefixLength % 8; // Set full bytes to 255 for (int i = 0; i < fullBytes; i++) { maskBytes[i] = 255; } // Set remaining bits if (remainingBits > 0 && fullBytes < maskBytes.Length) { maskBytes[fullBytes] = (byte)(255 << (8 - remainingBits)); } // Compare masked addresses for (int i = 0; i < clientBytes.Length; i++) { if ((clientBytes[i] & maskBytes[i]) != (networkBytes[i] & maskBytes[i])) { return false; } } return true; } catch (Exception ex) { _logger.LogError(ex, "Error checking CIDR range {CIDR}", cidr); return false; } } } /// /// Configuration options for IP whitelist middleware /// public class IPWhitelistOptions { public const string SectionName = "IPWhitelist"; /// /// Enable IP whitelist enforcement /// public bool Enabled { get; set; } = false; /// /// Use X-Forwarded-For and X-Real-IP headers (when behind reverse proxy) /// public bool UseForwardedHeaders { get; set; } = true; /// /// Show client IP in error response (for debugging) /// public bool ShowClientIP { get; set; } = false; /// /// List of whitelisted IP addresses /// public List WhitelistedIPs { get; set; } = new() { "127.0.0.1", "::1" }; /// /// List of whitelisted CIDR ranges /// public List WhitelistedCIDRs { get; set; } = new() { "192.168.0.0/16", // Private network Class C "10.0.0.0/8", // Private network Class A "172.16.0.0/12" // Private network Class B }; }