Files
ClawdDotNet/tests/ClawdDotNet.Tools.Tests/WebFetch/UrlGuardTests.cs
T
RichardandClaude Opus 5.5 77062facc9 Stufe 4c (#764): Bezeichner auf Englisch - Typen, Methoden, Felder, Parameter, Locals, Testnamen
Mit Roslyn umbenannt (Verweise folgen, Zeichenketten bleiben unberuehrt).
Nicht umbenannt, weil gespeichert oder ueber die API gebunden (Stufe 4e):
Properties, Record-Parameter, Enum-Member sowie die Parameter der
Endpunkt-Handler (Routen-/Query-Bindung ueber den Namen). Die JSON-Felder
anonymer Objekte bleiben ausdruecklich benannt (JsonFilter: pfad).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 13:18:43 +02:00

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 Addresses_in_the_local_or_private_network_are_blocked(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 Public_addresses_do_not_count_as_private(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 Only_http_and_https_are_allowed(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 Allowed_domains_and_their_subdomains_go_through(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 Lookalike_domains_are_rejected(string url)
{
Validate(url, out _).ShouldBeFalse();
}
[Fact]
public void Without_whitelist_nothing_is_let_through()
{
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 The_www_handling_does_not_mangle_other_host_names()
{
UrlGuard.IsDomainAllowed("mywww.example.com", Allowed).ShouldBeTrue();
UrlGuard.IsDomainAllowed("wwwxexample.com", Allowed).ShouldBeFalse();
}
[Fact]
public void A_trailing_dot_in_the_host_does_not_bypass_the_whitelist()
{
// "example.com." is the same host in DNS terms.
UrlGuard.IsDomainAllowed("evil.com.", Allowed).ShouldBeFalse();
UrlGuard.IsDomainAllowed("example.com.", Allowed).ShouldBeTrue();
}
[Fact]
public void Case_in_the_host_does_not_matter()
{
Validate("https://EXAMPLE.COM/x", out var error).ShouldBeTrue(error);
}
[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData("kein-url")]
[InlineData("/nur/ein/pfad")]
public void Invalid_inputs_are_rejected(string? url)
{
UrlGuard.TryValidate(url, Allowed, out _, out _).ShouldBeFalse();
}
}