Testing the SSRF handler works as expected

What follows is a sample console application using .NET 10's file based applications which opens an http listener on localhost, and attempts to connect to it, with and without the SSRF handler so you can see what happens.

Create a .NET .cs File

  1. At the command line change to the directory you want your app to run from run the following commands
    touch TestApp.cs
    chmod +x TestApp.cs
    

Paste the application code in TestApp.cs

  1. Open the TestApp.cs in your favourite editor and paste the following code
    #!/usr/bin/env dotnet
    
    #:sdk Microsoft.NET.Sdk.Web
    #:package idunno.Security.Ssrf@5.0.0
    #:property PublishAot=false
    using System.Net;
    using System.Text;
    using idunno.Security;
    
    Console.OutputEncoding = Encoding.UTF8;
    
    string hostUrl = "http://localhost:3000";
    
    var builder = WebApplication.CreateSlimBuilder(args);
    builder.Logging.ClearProviders();
    var app = builder.Build();
    app.Urls.Add(hostUrl);
    app.MapGet("/", async context =>
    {
        await context.Response.WriteAsync("Hello World!");
    });
    await app.StartAsync().ConfigureAwait(false);
    
    Console.Write($"Kestrel listening on ");
    foreach (var url in app.Urls)
    {
        Console.Write($"{url} ");
    }
    Console.WriteLine();
    Console.WriteLine();
    
    try
    {
        using (var client = new HttpClient())
        {
            Console.WriteLine($"Making request to {hostUrl} without the SSRF handler.\nShould succeed.");
            var getResult = await client.GetAsync(hostUrl);
            Console.WriteLine($"✔️ Status Code: {getResult.StatusCode}");
        }
    }
    catch (Exception ex)
    {
        var indent = 2;
    
        Console.WriteLine($"❌ {ex.GetType().Name}: {ex.Message}");
    
        while (ex.InnerException is not null)
        {
            indent += 2;
            ex = ex.InnerException;
    
            Console.Write(new string(' ', indent));
            Console.Write($"↳ {ex.GetType().Name} => {ex.Message}");
        }
    }
    
    Console.WriteLine();
    
    try
    {
        using (var client = new HttpClient(SsrfSocketsHttpHandlerFactory.Create()))
        {
            Console.WriteLine($"Making request to {hostUrl} with the SSRF handler.\nShould fail.");
            var getResult = await client.GetAsync(hostUrl);
            Console.WriteLine($"✔️ Status Code: {getResult.StatusCode}");
        }
    }
    catch (Exception ex)
    {
        var indent = 2;
    
        Console.WriteLine($"❌ {ex.GetType().Name}: {ex.Message}");
    
        while (ex.InnerException is not null)
        {
            indent += 2;
            ex = ex.InnerException;
    
            Console.Write(new string(' ', indent));
            Console.WriteLine($"↳ {ex.GetType().Name} => {ex.Message}");
        }
    }
    
    Console.WriteLine();
    
    try
    {
        using (var client = new HttpClient(SsrfSocketsHttpHandlerFactory.Create(allowedSchemes: ["http", "https"])))
        {
            Console.WriteLine($"Making request to {hostUrl} with the SSRF handler, allowing http.\nShould fail");
            var getResult = await client.GetAsync(hostUrl);
            Console.WriteLine($"✔️ Status Code: {getResult.StatusCode}");
        }
    }
    catch (Exception ex)
    {
        var indent = 2;
    
        Console.WriteLine($"❌ {ex.GetType().Name}: {ex.Message}");
    
        while (ex.InnerException is not null)
        {
            indent += 2;
            ex = ex.InnerException;
    
            Console.Write(new string(' ', indent));
            Console.WriteLine($"↳ {ex.GetType().Name} => {ex.Message}");
        }
    }
    
    Console.WriteLine();
    
    hostUrl = "http://loopback.ssrf.fail:3000";
    try
    {
        using (var client = new HttpClient(SsrfSocketsHttpHandlerFactory.Create(allowedSchemes: ["http", "https"])))
        {
            Console.WriteLine($"Making request to {hostUrl} with the SSRF handler, allowing http.\nShould fail.");
            var getResult = await client.GetAsync(hostUrl);
            Console.WriteLine($"✔️ Status Code: {getResult.StatusCode}");
        }
    }
    catch (Exception ex)
    {
        var indent = 2;
    
        Console.WriteLine($"❌ {ex.GetType().Name}: {ex.Message}");
    
        while (ex.InnerException is not null)
        {
            indent += 2;
            ex = ex.InnerException;
    
            Console.Write(new string(' ', indent));
            Console.WriteLine($"↳ {ex.GetType().Name} => {ex.Message}");
        }
    }
    
    Console.WriteLine();
    
    hostUrl = "http://loopback.ssrf.fail:3000";
    try
    {
        using (var client = new HttpClient(SsrfSocketsHttpHandlerFactory.Create(
                allowedHostnames: ["*.ssrf.fail"],
                connectTimeout: new TimeSpan(0, 0, 5),
                allowedSchemes: ["http", "https"],
                automaticDecompression: DecompressionMethods.All)))
        {
            Console.WriteLine($"Making request to {hostUrl} with the SSRF handler, safe listing *.ssrf.fail and allowing http.\nShould succeed.");
            var getResult = await client.GetAsync(hostUrl);
            Console.WriteLine($"✔️ Status Code: {getResult.StatusCode}");
        }
    }
    catch (Exception ex)
    {
        var indent = 0;
    
        Console.WriteLine($"❌ {ex.GetType().Name}: {ex.Message}");
    
        while (ex.InnerException is not null)
        {
            indent += 2;
            ex = ex.InnerException;
    
            Console.Write(new string(' ', indent));
            Console.WriteLine($"↳ {ex.GetType().Name} => {ex.Message}");
        }
    }
    
    await app.StopAsync().ConfigureAwait(false);
    
  2. Save the file

Run the application

  1. At the command line change to the directory you want your app to run from run the following commands
    dotnet run TestApp.cs
    

What you will see is a request with a plain HttpClient being made to http://localhost:3000 with no SSRF protection, which works, and gets an HTTP 200 response.

Next the same request is made, but with an HttpClient that has the SSRF handler. An exception is thrown because the URI is considered unsafe, as it is http and not https.

Next the same request is made, but with an HttpClient that has the SSRF handler and configured to allow insecure protocols and attempts to send the same request, at which point an exception is thrown because the URI is considered unsafe, as it is a loopback address.

Finally it creates an HttpClient with SSRF protection, allowing insecure protocols, but this time sends the request to http://loopback.ssrf.fail:3000. loopback.ssrf.fail is a test DNS entry that resolves to both the ipv4 and ipv6 loopback addresses. This time an SsrfException is thrown but the message is different, "Connection blocked as all resolved addresses are unsafe", showing that the URI passed inspection, but the IP addresses it resolved to did not.