Add trusted forwarded-header config and tests
This commit is contained in:
@@ -4,9 +4,13 @@ using System.Reflection;
|
||||
using GameList.Infrastructure;
|
||||
using GameList.Endpoints;
|
||||
using GameList.Tests.Support;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.HttpOverrides;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.TestHost;
|
||||
using Microsoft.Extensions.FileProviders;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using System.Text.Json;
|
||||
using System.Net.Http.Json;
|
||||
|
||||
@@ -194,6 +198,60 @@ public class HelperTests
|
||||
Assert.Equal("Unexpected server error", json.GetProperty("error").GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildForwardedHeadersOptions_reads_only_valid_proxies_and_networks()
|
||||
{
|
||||
var config = new ConfigurationBuilder().AddInMemoryCollection(new Dictionary<string, string?>
|
||||
{
|
||||
["ForwardedHeaders:KnownProxies:0"] = "203.0.113.10",
|
||||
["ForwardedHeaders:KnownProxies:1"] = "not-an-ip",
|
||||
["ForwardedHeaders:KnownNetworks:0"] = "10.20.0.0/16",
|
||||
["ForwardedHeaders:KnownNetworks:1"] = "invalid"
|
||||
}).Build();
|
||||
|
||||
var options = BuildForwardedHeadersOptionsForTest(config);
|
||||
|
||||
Assert.Single(options.KnownProxies);
|
||||
Assert.Single(options.KnownIPNetworks);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Forwarded_headers_are_ignored_for_untrusted_proxy()
|
||||
{
|
||||
var config = new ConfigurationBuilder().AddInMemoryCollection(new Dictionary<string, string?>
|
||||
{
|
||||
["ForwardedHeaders:KnownProxies:0"] = "203.0.113.10"
|
||||
}).Build();
|
||||
var options = BuildForwardedHeadersOptionsForTest(config);
|
||||
|
||||
var builder = WebApplication.CreateBuilder();
|
||||
builder.WebHost.UseTestServer();
|
||||
|
||||
await using var app = builder.Build();
|
||||
app.Use((ctx, next) =>
|
||||
{
|
||||
ctx.Connection.RemoteIpAddress = IPAddress.Parse("198.51.100.25");
|
||||
return next();
|
||||
});
|
||||
app.UseForwardedHeaders(options);
|
||||
app.MapGet("/scheme", (HttpContext ctx) => ctx.Request.Scheme);
|
||||
|
||||
await app.StartAsync();
|
||||
try
|
||||
{
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, "/scheme");
|
||||
request.Headers.TryAddWithoutValidation("X-Forwarded-Proto", "https");
|
||||
var response = await app.GetTestClient().SendAsync(request);
|
||||
var scheme = await response.Content.ReadAsStringAsync();
|
||||
|
||||
Assert.Equal("http", scheme);
|
||||
}
|
||||
finally
|
||||
{
|
||||
await app.StopAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private class FakeEnv : IWebHostEnvironment
|
||||
{
|
||||
public string ApplicationName { get; set; } = "";
|
||||
@@ -204,6 +262,12 @@ public class HelperTests
|
||||
public IFileProvider ContentRootFileProvider { get; set; } = null!;
|
||||
}
|
||||
|
||||
private static ForwardedHeadersOptions BuildForwardedHeadersOptionsForTest(IConfiguration config)
|
||||
{
|
||||
var method = typeof(Program).GetMethods(BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public).First(m => m.Name.Contains("BuildForwardedHeadersOptions"));
|
||||
return (ForwardedHeadersOptions)method.Invoke(null, [config])!;
|
||||
}
|
||||
|
||||
private sealed class RedirectFollowingHandler : HttpMessageHandler
|
||||
{
|
||||
private static readonly Uri Source = new("http://example.com/img.png");
|
||||
|
||||
3
IIS.md
3
IIS.md
@@ -13,6 +13,9 @@
|
||||
- `ASPNETCORE_ENVIRONMENT=Production`
|
||||
- `ADMIN_PASSWORD=<your-secret>`
|
||||
- `BasePath=/picknplay` (only if the site is under a subfolder; omit for root)
|
||||
- Configure trusted reverse proxies/networks for forwarded headers (do not trust all sources):
|
||||
- `ForwardedHeaders__KnownProxies__0=10.0.0.10`
|
||||
- `ForwardedHeaders__KnownNetworks__0=10.0.0.0/24`
|
||||
- Optional: enable stdout logging in `web.config` during troubleshooting only; disable afterward.
|
||||
- Data protection keys are persisted to `App_Data/keys`; ensure this folder is deployed and writable so auth cookies stay valid across app pool recycles.
|
||||
- Frontend base path: set `<meta name="app-base" content="/picknplay">` in `wwwroot/index.html` for production so API calls include the subpath (keep blank for local/root).
|
||||
|
||||
37
Program.cs
37
Program.cs
@@ -6,6 +6,7 @@ using Microsoft.AspNetCore.DataProtection;
|
||||
using Microsoft.AspNetCore.HttpOverrides;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Net;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
@@ -66,7 +67,7 @@ builder.Services.AddAuthorization(options => { options.AddPolicy(PlayerIdentityE
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
app.UseForwardedHeaders(new ForwardedHeadersOptions { ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto | ForwardedHeaders.XForwardedHost });
|
||||
app.UseForwardedHeaders(BuildForwardedHeadersOptions(builder.Configuration));
|
||||
|
||||
var basePath = builder.Configuration["BasePath"];
|
||||
if (!string.IsNullOrWhiteSpace(basePath))
|
||||
@@ -100,6 +101,40 @@ app.MapAdminEndpoints();
|
||||
|
||||
app.Run();
|
||||
|
||||
static ForwardedHeadersOptions BuildForwardedHeadersOptions(IConfiguration config)
|
||||
{
|
||||
var options = new ForwardedHeadersOptions
|
||||
{
|
||||
ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto | ForwardedHeaders.XForwardedHost
|
||||
};
|
||||
|
||||
options.KnownIPNetworks.Clear();
|
||||
options.KnownProxies.Clear();
|
||||
|
||||
foreach (var proxy in config.GetSection("ForwardedHeaders:KnownProxies").Get<string[]>() ?? [])
|
||||
{
|
||||
if (IPAddress.TryParse(proxy, out var parsedProxy))
|
||||
options.KnownProxies.Add(parsedProxy);
|
||||
}
|
||||
|
||||
foreach (var network in config.GetSection("ForwardedHeaders:KnownNetworks").Get<string[]>() ?? [])
|
||||
{
|
||||
var parts = network.Split('/', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
|
||||
if (parts.Length != 2)
|
||||
continue;
|
||||
|
||||
if (!IPAddress.TryParse(parts[0], out var prefix))
|
||||
continue;
|
||||
|
||||
if (!int.TryParse(parts[1], out var prefixLength))
|
||||
continue;
|
||||
|
||||
options.KnownIPNetworks.Add(new System.Net.IPNetwork(prefix, prefixLength));
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
static void UpdateIndexMetaBase(IWebHostEnvironment env, string basePath)
|
||||
{
|
||||
try
|
||||
|
||||
Reference in New Issue
Block a user