Rund 2.600 Kommentarblöcke in 310 C#-Dateien übersetzt, Begründungen vollständig erhalten: blockweise gleiche Anzahl wie vorher, kein Block unter 60 % seiner Länge, der Code außerhalb der Kommentare per Tokenizer byte-identisch geprüft. Zitate deutscher Oberflächentexte und Testdaten bleiben deutsch. WebUI: Kommentare in JS/CSS/HTML übersetzt, JSDoc-@param an die in 4b umbenannten Parameter angeglichen. Dabei behoben: - 4b hatte "tag" (HTML-Tag) als deutsches "Tag" gelesen und zu "dayValue" gemacht (makeElement, svg, Tagesübersicht) - zurück auf tag/day. - Aufgaben "InProgress" bekamen die Plakettenart "laufend", für die es nie eine CSS-Klasse gab - jetzt "success" wie laufende Instanzen. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
131 lines
5.1 KiB
C#
131 lines
5.1 KiB
C#
using ClawdDotNet.Tools.WebFetch;
|
|
using Shouldly;
|
|
|
|
namespace ClawdDotNet.Tools.Tests.WebFetch;
|
|
|
|
/// <summary>
|
|
/// S5 from the inventory: the whitelist was only applied to the initial URL,
|
|
/// but HttpClient followed redirects itself. An allowed domain could thus redirect into the
|
|
/// local network — router, NAS, the own Git server, cloud metadata services.
|
|
/// </summary>
|
|
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);
|
|
|
|
// ═══════════════════════════════════════════════════════════
|
|
// Network blocks
|
|
// ═══════════════════════════════════════════════════════════
|
|
|
|
[Theory]
|
|
[InlineData("http://169.254.169.254/latest/meta-data/")] // cloud metadata
|
|
[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")] // just below 172.16/12
|
|
[InlineData("172.32.0.1")] // just above
|
|
[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();
|
|
}
|
|
|
|
/// <summary>
|
|
/// The old version removed "www." via a replace across the whole string:
|
|
/// "mywww.example.com" became "myexample.com".
|
|
/// </summary>
|
|
[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." is the same host in DNS terms.
|
|
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();
|
|
}
|
|
}
|