0954876295
Implement B01-B06 with regression coverage and retained validation evidence. Preserve the production ingress preflight; F04 runtime verification remains open.
89 lines
3.6 KiB
C#
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));
|
|
}
|
|
}
|