diff --git a/src/Backend/AutopilotMonitor.Functions.Tests/SsrfGuardTests.cs b/src/Backend/AutopilotMonitor.Functions.Tests/SsrfGuardTests.cs
index 74e67d3d..d8a3523c 100644
--- a/src/Backend/AutopilotMonitor.Functions.Tests/SsrfGuardTests.cs
+++ b/src/Backend/AutopilotMonitor.Functions.Tests/SsrfGuardTests.cs
@@ -1,8 +1,14 @@
using System.Net;
+using System.Text;
+using System.Text.RegularExpressions;
using AutopilotMonitor.Functions.Security;
namespace AutopilotMonitor.Functions.Tests;
+///
+/// Format checks and the address classifier. The connection gate itself is covered on real sockets
+/// in .
+///
public class SsrfGuardTests
{
// ── ValidateWebhookUrlFormat: valid URLs ──
@@ -145,37 +151,124 @@ public class SsrfGuardTests
Assert.False(SsrfGuard.IsBlockedAddress(IPAddress.Parse(ip)));
}
- // ── ValidateDestinationAsync: integration ──
+ // ── IsBlockedAddress: IPv6 beyond the classic ranges ──
- [Fact]
- public async Task ValidateDestination_Localhost_ThrowsSsrfException()
+ [Theory]
+ [InlineData("::")] // unspecified
+ [InlineData("::ffff:8.8.8.8")] // IPv4-mapped, even of a public address
+ [InlineData("::8.8.8.8")] // IPv4-compatible (deprecated)
+ [InlineData("64:ff9b::a00:1")] // NAT64 of 10.0.0.1
+ [InlineData("2002:a00:1::1")] // 6to4 of 10.0.0.1
+ [InlineData("2001::1")] // Teredo
+ [InlineData("2001:db8::1")] // documentation
+ [InlineData("fec0::1")] // deprecated site-local
+ [InlineData("4000::1")] // outside global unicast
+ [InlineData("e000::1")] // outside global unicast
+ public void IsBlocked_Ipv6OutsidePublicUnicast_ReturnsTrue(string ip)
{
- var ex = await Assert.ThrowsAsync(
- () => SsrfGuard.ValidateDestinationAsync("https://localhost/webhook"));
- Assert.Contains("private or reserved", ex.Message);
+ Assert.True(SsrfGuard.IsBlockedAddress(IPAddress.Parse(ip)));
}
- [Fact]
- public async Task ValidateDestination_HttpScheme_ThrowsSsrfException()
+ [Theory]
+ [InlineData("fe80::1%5")]
+ [InlineData("2606:4700:4700::1111%5")]
+ public void IsBlocked_Ipv6WithZoneId_ReturnsTrue(string ip)
{
- var ex = await Assert.ThrowsAsync(
- () => SsrfGuard.ValidateDestinationAsync("http://example.com/webhook"));
- Assert.Contains("HTTPS", ex.Message);
+ Assert.True(SsrfGuard.IsBlockedAddress(IPAddress.Parse(ip)));
}
- [Fact]
- public async Task ValidateDestination_InvalidUrl_ThrowsSsrfException()
+ [Theory]
+ [InlineData("2606:4700:4700::1111")] // Cloudflare
+ [InlineData("2a00:1450:4001::1")] // Google
+ [InlineData("2001:200::1")] // just above 2001::/23
+ [InlineData("2003::1")] // just above 6to4 2002::/16
+ [InlineData("2001:db9::1")] // just above documentation 2001:db8::/32
+ [InlineData("192.0.1.0")] // just above 192.0.0.0/24
+ [InlineData("198.20.0.0")] // just above benchmarking 198.18.0.0/15
+ [InlineData("223.255.255.255")] // just below multicast
+ public void IsBlocked_PublicNeighbourOfRefusedBlock_ReturnsFalse(string ip)
{
- await Assert.ThrowsAsync(
- () => SsrfGuard.ValidateDestinationAsync("not-a-url"));
+ Assert.False(SsrfGuard.IsBlockedAddress(IPAddress.Parse(ip)));
}
+ // ── IsBlockedAddress: every IANA special-purpose block (tests/fixtures/iana-special-purpose-addresses) ──
+
+ private static readonly string[] RegistryFiles = { "iana-ipv4-special-registry-1.csv", "iana-ipv6-special-registry-1.csv" };
+
+ public static IEnumerable