using System.Net; using FluentAssertions; using InboxIntel.Infrastructure.Security; using Xunit; namespace InboxIntel.UnitTests; /// /// SSRF guard (V-01): outbound URLs derived from attacker-authored email headers /// must not be allowed to target internal/metadata/private addresses or non-web schemes. /// public class SafeHttpGuardTests { [Theory] [InlineData("169.254.169.254")] // cloud metadata [InlineData("127.0.0.1")] // loopback [InlineData("10.0.0.5")] // private A [InlineData("172.16.4.4")] // private B [InlineData("172.31.255.255")] // private B upper bound [InlineData("192.168.1.1")] // private C [InlineData("100.64.0.1")] // CGNAT [InlineData("0.0.0.0")] // this-host [InlineData("255.255.255.255")] // broadcast [InlineData("224.0.0.1")] // multicast public void Blocks_private_and_special_ipv4(string ip) => SafeHttpGuard.IsBlocked(IPAddress.Parse(ip)).Should().BeTrue(); [Theory] [InlineData("::1")] // loopback [InlineData("fe80::1")] // link-local [InlineData("fc00::1")] // unique-local [InlineData("fd12:3456::1")] // unique-local [InlineData("::ffff:169.254.169.254")] // v4-mapped metadata [InlineData("::ffff:10.0.0.1")] // v4-mapped private public void Blocks_private_and_special_ipv6(string ip) => SafeHttpGuard.IsBlocked(IPAddress.Parse(ip)).Should().BeTrue(); [Theory] [InlineData("8.8.8.8")] [InlineData("93.184.216.34")] // example.com [InlineData("2606:2800:220:1::1")] public void Allows_public_addresses(string ip) => SafeHttpGuard.IsBlocked(IPAddress.Parse(ip)).Should().BeFalse(); [Theory] [InlineData("ftp://example.com/x")] [InlineData("file:///etc/passwd")] [InlineData("gopher://example.com")] [InlineData("not-a-url")] [InlineData("")] public async Task Rejects_non_http_schemes_and_garbage(string url) { var act = async () => await SafeHttpGuard.ValidateAsync(url); await act.Should().ThrowAsync(); } [Fact] public async Task Rejects_url_resolving_to_loopback() { // localhost resolves to a loopback address and must be blocked. var act = async () => await SafeHttpGuard.ValidateAsync("http://localhost/unsub"); await act.Should().ThrowAsync(); } [Fact] public async Task Rejects_literal_metadata_ip_url() { var act = async () => await SafeHttpGuard.ValidateAsync("http://169.254.169.254/latest/meta-data/"); await act.Should().ThrowAsync(); } }