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(); } }