|
1 | 1 | using System.Net; |
2 | 2 | using System.Text.Json; |
3 | 3 | using System.Text.Json.Serialization; |
| 4 | +using Microsoft.AspNetCore.HostFiltering; |
4 | 5 | using Microsoft.AspNetCore.HttpOverrides; |
5 | 6 | using TrendWeight.Infrastructure.Extensions; |
6 | 7 | using TrendWeight.Infrastructure.Middleware; |
7 | 8 |
|
8 | 9 | var builder = WebApplication.CreateBuilder(args); |
9 | 10 |
|
| 11 | +// Disable ASP.NET Core's built-in host filtering since we have custom validation |
| 12 | +builder.Services.Configure<HostFilteringOptions>(options => |
| 13 | +{ |
| 14 | + options.AllowedHosts = new List<string> { "*" }; |
| 15 | +}); |
| 16 | + |
10 | 17 | // Add services to the container |
11 | 18 | builder.Services.AddControllers() |
12 | 19 | .AddJsonOptions(options => |
|
55 | 62 | { |
56 | 63 | options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto | ForwardedHeaders.XForwardedHost; |
57 | 64 |
|
58 | | - if (builder.Environment.IsProduction()) |
59 | | - { |
60 | | - // Production: Only trust the immediate proxy (most secure default) |
61 | | - options.ForwardLimit = 1; |
62 | | - options.RequireHeaderSymmetry = false; |
63 | | - } |
64 | | - else |
65 | | - { |
66 | | - // Development: Trust any source for ease of testing |
67 | | - options.KnownNetworks.Clear(); |
68 | | - options.KnownProxies.Clear(); |
69 | | - } |
| 65 | + // Clear default networks/proxies to trust headers from load balancers |
| 66 | + // Security Note: Host header validation is performed later in the pipeline |
| 67 | + options.KnownNetworks.Clear(); |
| 68 | + options.KnownProxies.Clear(); |
| 69 | + |
| 70 | + // Limit proxy chain depth to prevent spoofing |
| 71 | + options.ForwardLimit = 2; // Allows for Cloudflare -> DigitalOcean chain |
| 72 | + options.RequireHeaderSymmetry = false; |
70 | 73 | }); |
71 | 74 |
|
72 | 75 | var app = builder.Build(); |
|
80 | 83 | // Use forwarded headers from proxies |
81 | 84 | app.UseForwardedHeaders(); |
82 | 85 |
|
| 86 | +// Validate host header for security (prevent host header injection) |
| 87 | +if (app.Environment.IsProduction()) |
| 88 | +{ |
| 89 | + app.Use(async (context, next) => |
| 90 | + { |
| 91 | + var allowedHostsConfig = app.Configuration["AllowedHosts"]; |
| 92 | + |
| 93 | + if (!string.IsNullOrEmpty(allowedHostsConfig) && allowedHostsConfig != "*") |
| 94 | + { |
| 95 | + var allowedHosts = allowedHostsConfig.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); |
| 96 | + var requestHost = context.Request.Host.Host; |
| 97 | + |
| 98 | + // Check both with and without port |
| 99 | + var hostMatches = allowedHosts.Any(h => |
| 100 | + h.Equals(requestHost, StringComparison.OrdinalIgnoreCase) || |
| 101 | + h.Equals(context.Request.Host.Value, StringComparison.OrdinalIgnoreCase)); |
| 102 | + |
| 103 | + if (!hostMatches) |
| 104 | + { |
| 105 | + app.Logger.LogWarning("Rejected request with invalid host header: {Host}", context.Request.Host.Value); |
| 106 | + context.Response.StatusCode = 400; |
| 107 | + await context.Response.WriteAsync("Bad Request: Invalid Host header"); |
| 108 | + return; |
| 109 | + } |
| 110 | + } |
| 111 | + await next(); |
| 112 | + }); |
| 113 | +} |
| 114 | + |
83 | 115 | if (app.Environment.IsDevelopment()) |
84 | 116 | { |
85 | 117 | app.UseSwagger(); |
|
0 commit comments