Files
jobtrackingapp/JobTrackerApi.Tests/ForwardedIngressTests.cs
cesnimda 0954876295 fix(security): enforce audited account, network and resource boundaries
Implement B01-B06 with regression coverage and retained validation evidence. Preserve the production ingress preflight; F04 runtime verification remains open.
2026-09-09 18:21:47 +02:00

89 lines
3.6 KiB
C#

using System.Net;
using JobTrackerApi.Services;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Xunit;
namespace JobTrackerApi.Tests;
public sealed class ForwardedIngressTests
{
private static async Task<DefaultHttpContext> Forward(string peer, string forwardedFor, string proto = "https")
{
var config = new ConfigurationBuilder().AddInMemoryCollection(new Dictionary<string, string?>
{
["Proxy:KnownNetworks:0"] = "172.31.250.0/29",
}).Build();
var context = new DefaultHttpContext();
context.Connection.RemoteIpAddress = IPAddress.Parse(peer);
context.Request.Scheme = "http";
context.Request.Host = new HostString("jobs.example.test");
context.Request.Headers["X-Forwarded-For"] = forwardedFor;
context.Request.Headers["X-Forwarded-Proto"] = proto;
context.Request.Headers["X-Forwarded-Host"] = "attacker.example.test";
context.Request.Headers["Forwarded"] = "for=203.0.113.99;proto=https;host=attacker.example.test";
var middleware = new ForwardedHeadersMiddleware(_ => Task.CompletedTask, NullLoggerFactory.Instance,
Options.Create(ForwardedProxyConfiguration.Build(config)));
await middleware.Invoke(context);
Assert.Equal("jobs.example.test", context.Request.Host.Value);
return context;
}
[Theory]
[InlineData("172.31.250.2", "198.51.100.10")]
[InlineData("172.31.250.2", "198.51.100.11")]
[InlineData("::ffff:172.31.250.2", "2001:db8::1")]
public async Task Trusted_nginx_peer_resolves_client_and_scheme_but_never_forwarded_host(string peer, string client)
{
var context = await Forward(peer, client);
Assert.Equal(IPAddress.Parse(client), context.Connection.RemoteIpAddress);
Assert.Equal("https", context.Request.Scheme);
}
[Theory]
[InlineData("198.51.100.20")]
[InlineData("172.31.251.2")]
[InlineData("::ffff:198.51.100.20")]
[InlineData("2001:db8::20")]
public async Task Untrusted_socket_peer_cannot_forge_IP_scheme_or_host(string peer)
{
var context = await Forward(peer, "203.0.113.99");
Assert.Equal(IPAddress.Parse(peer), context.Connection.RemoteIpAddress);
Assert.Equal("http", context.Request.Scheme);
}
[Theory]
[InlineData("not-an-ip")]
[InlineData("999.999.999.999")]
[InlineData("198.51.100.1, invalid")]
public async Task Invalid_rightmost_address_cannot_manufacture_a_client(string input)
{
var context = await Forward("172.31.250.2", input);
Assert.Equal(IPAddress.Parse("172.31.250.2"), context.Connection.RemoteIpAddress);
Assert.Equal("http", context.Request.Scheme);
}
[Fact]
public async Task Backend_defence_consumes_only_one_hop_even_if_nginx_contract_is_broken()
{
var context = await Forward("172.31.250.2", "203.0.113.99, 198.51.100.10");
Assert.Equal(IPAddress.Parse("198.51.100.10"), context.Connection.RemoteIpAddress);
Assert.Equal("203.0.113.99", context.Request.Headers["X-Forwarded-For"]);
}
[Theory]
[InlineData("0.0.0.0/0")]
[InlineData("::/0")]
public void Trusting_the_entire_internet_fails_configuration(string value)
{
var config = new ConfigurationBuilder().AddInMemoryCollection(new Dictionary<string, string?>
{
["Proxy:KnownNetworks:0"] = value,
}).Build();
Assert.Throws<InvalidOperationException>(() => ForwardedProxyConfiguration.Build(config));
}
}