using ClawdDotNet.Tools.WebFetch;
using Shouldly;
namespace ClawdDotNet.Tools.Tests.WebFetch;
///
/// S5 aus der Bestandsaufnahme: Die Whitelist wurde nur auf die Ausgangs-URL angewendet,
/// HttpClient folgte Weiterleitungen aber selbst. Eine erlaubte Domain konnte damit ins
/// lokale Netz weiterleiten — Router, NAS, der eigene Git-Server, Cloud-Metadatendienste.
///
public sealed class UrlGuardTests
{
private static readonly string[] Allowed = ["example.com", "reuters.com"];
private static bool Validate(string url, out string? error)
=> UrlGuard.TryValidate(url, Allowed, out _, out error);
// ═══════════════════════════════════════════════════════════
// Netzsperren
// ═══════════════════════════════════════════════════════════
[Theory]
[InlineData("http://169.254.169.254/latest/meta-data/")] // Cloud-Metadaten
[InlineData("http://127.0.0.1:8080/")]
[InlineData("http://localhost:8418/")]
[InlineData("http://192.168.178.10:8418/Richard/ClawdDotNet.git")]
[InlineData("http://10.0.0.5/admin")]
[InlineData("http://172.16.4.2/")]
[InlineData("http://172.31.255.255/")]
[InlineData("http://0.0.0.0/")]
[InlineData("http://100.64.1.1/")]
[InlineData("http://[::1]/")]
[InlineData("http://[fd00::1]/")]
[InlineData("http://nas.local/")]
[InlineData("http://dienst.internal/")]
public void Adressen_im_lokalen_oder_privaten_Netz_werden_gesperrt(string url)
{
Validate(url, out var error).ShouldBeFalse($"'{url}' muss gesperrt sein");
error.ShouldNotBeNull();
}
[Theory]
[InlineData("172.15.0.1")] // knapp unterhalb von 172.16/12
[InlineData("172.32.0.1")] // knapp oberhalb
[InlineData("11.0.0.1")]
[InlineData("8.8.8.8")]
public void Oeffentliche_Adressen_gelten_nicht_als_privat(string host)
{
UrlGuard.IsPrivateOrLocal(host).ShouldBeFalse();
}
// ═══════════════════════════════════════════════════════════
// Schema
// ═══════════════════════════════════════════════════════════
[Theory]
[InlineData("file:///C:/Windows/win.ini")]
[InlineData("ftp://example.com/datei")]
[InlineData("gopher://example.com/")]
public void Nur_http_und_https_sind_erlaubt(string url)
{
Validate(url, out _).ShouldBeFalse();
}
// ═══════════════════════════════════════════════════════════
// Whitelist
// ═══════════════════════════════════════════════════════════
[Theory]
[InlineData("https://example.com/artikel")]
[InlineData("https://www.example.com/artikel")]
[InlineData("https://news.example.com/artikel")]
[InlineData("https://a.b.example.com/x")]
[InlineData("https://reuters.com/")]
public void Erlaubte_Domains_und_ihre_Subdomains_gehen_durch(string url)
{
Validate(url, out var error).ShouldBeTrue(error);
}
[Theory]
[InlineData("https://example.com.attacker.net/x")]
[InlineData("https://notexample.com/x")]
[InlineData("https://evil.com/x")]
[InlineData("https://exampleXcom/x")]
public void Aehnlich_aussehende_Domains_werden_abgelehnt(string url)
{
Validate(url, out _).ShouldBeFalse();
}
[Fact]
public void Ohne_Whitelist_wird_nichts_durchgelassen()
{
UrlGuard.TryValidate("https://example.com", [], out _, out _).ShouldBeFalse();
}
///
/// Die alte Fassung entfernte "www." per Ersetzen über die ganze Zeichenkette:
/// aus "mywww.example.com" wurde "myexample.com".
///
[Fact]
public void Die_www_Behandlung_verstuemmelt_keine_anderen_Hostnamen()
{
UrlGuard.IsDomainAllowed("mywww.example.com", Allowed).ShouldBeTrue();
UrlGuard.IsDomainAllowed("wwwxexample.com", Allowed).ShouldBeFalse();
}
[Fact]
public void Ein_abschliessender_Punkt_im_Host_umgeht_die_Whitelist_nicht()
{
// "example.com." ist DNS-technisch derselbe Host.
UrlGuard.IsDomainAllowed("evil.com.", Allowed).ShouldBeFalse();
UrlGuard.IsDomainAllowed("example.com.", Allowed).ShouldBeTrue();
}
[Fact]
public void Grossschreibung_im_Host_spielt_keine_Rolle()
{
Validate("https://EXAMPLE.COM/x", out var error).ShouldBeTrue(error);
}
[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData("kein-url")]
[InlineData("/nur/ein/pfad")]
public void Ungueltige_Eingaben_werden_abgelehnt(string? url)
{
UrlGuard.TryValidate(url, Allowed, out _, out _).ShouldBeFalse();
}
}