Befunde aus dem Testbetrieb auf der Linux-VM behoben (#572)
- #578: Abgebrochene Laeufe (Schrittgrenze, Zeitueberschreitung, Abbruch, Ausnahme) tragen ihre bezahlten Token - vorher 0 in RunUsage, fuer das Budget unsichtbar. - #580: WebFetch prueft die aufgeloeste Zieladresse beim Verbinden; ein erlaubter Name, der auf 127.0.0.1 aufloest, wird gesperrt. - #575: Dateien entstehen unter Unix mit 0600 (AtomicFile, Protokolle, FileRW-Anhaengen, state.db), Protokollverzeichnisse mit 0700. - #576: SIGTERM/SIGINT beenden auch mit Fenster geordnet. - #593: OpenRouter-Statusdienst wird gestartet. - #589: Auftragsnachricht nennt aktuelle Zeit, Zeitzone und Termin. - #581: Unvollstaendiges Argument-JSON erreicht das Werkzeug nicht mehr; das Modell bekommt eine verstaendliche Rueckmeldung statt Parsertext. - #591: Weboberflaeche liest die Felder, die API und Ereignisstrom liefern (Freigaben, Protokoll, Taskboard mit Termin, Werkzeug-Einstellungen, Tokennutzung); Ereignisstrom in camelCase. - #592: Chat zeigt Nachrichten nicht mehr doppelt. - #536 (teilweise): A2A-Antworten tragen Absender und Empfaenger, Parser wirft nicht mehr; Datums- und Volltextfilter in Agenten-Chats. - deploy/publish.py: Packager-Pfad net10.0, Schalter --agent-file. - Version 0.1.4. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Sockets;
|
||||
|
||||
namespace ClawdDotNet.Tools.WebFetch;
|
||||
@@ -14,6 +15,14 @@ namespace ClawdDotNet.Tools.WebFetch;
|
||||
///
|
||||
/// Deshalb: Weiterleitungen werden nicht mehr automatisch verfolgt, sondern einzeln
|
||||
/// aufgelöst und JEDER Zwischenschritt erneut geprüft.
|
||||
///
|
||||
/// <para><b>Und die Adresse wird dort geprüft, wo verbunden wird (#580).</b> Die Prüfung der
|
||||
/// URL sieht nur den Namen. <c>localtest.me</c> steht als Name nirgends auf einer Sperrliste
|
||||
/// und löst doch auf <c>127.0.0.1</c> auf — im Prüfdurchgang hat WebFetch genau so eine
|
||||
/// Verbindung auf Loopback geöffnet. <see cref="ConnectAsync"/> löst den Namen selbst auf,
|
||||
/// prüft jede Zieladresse und verbindet nur mit einer geprüften. Damit greift auch
|
||||
/// DNS-Rebinding nicht mehr: Eine zweite Auflösung zwischen Prüfung und Verbindung gibt es
|
||||
/// nicht.</para>
|
||||
/// </summary>
|
||||
public static class UrlGuard
|
||||
{
|
||||
@@ -114,9 +123,12 @@ public static class UrlGuard
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!IPAddress.TryParse(value, out var ip))
|
||||
return false;
|
||||
return IPAddress.TryParse(value, out var ip) && IsPrivateOrLocal(ip);
|
||||
}
|
||||
|
||||
/// <summary>Dieselbe Sperre für eine bereits aufgelöste Adresse.</summary>
|
||||
public static bool IsPrivateOrLocal(IPAddress ip)
|
||||
{
|
||||
if (IPAddress.IsLoopback(ip))
|
||||
return true;
|
||||
|
||||
@@ -138,7 +150,7 @@ public static class UrlGuard
|
||||
|
||||
if (ip.AddressFamily == AddressFamily.InterNetworkV6)
|
||||
{
|
||||
if (ip.IsIPv6LinkLocal || ip.IsIPv6SiteLocal)
|
||||
if (ip.IsIPv6LinkLocal || ip.IsIPv6SiteLocal || ip.Equals(IPAddress.IPv6None))
|
||||
return true;
|
||||
|
||||
// Unique Local Addresses fc00::/7
|
||||
@@ -148,9 +160,70 @@ public static class UrlGuard
|
||||
|
||||
// IPv4-gemappte Adressen erneut als IPv4 prüfen
|
||||
if (ip.IsIPv4MappedToIPv6)
|
||||
return IsPrivateOrLocal(ip.MapToIPv4().ToString());
|
||||
return IsPrivateOrLocal(ip.MapToIPv4());
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verbindungsaufbau für den <see cref="SocketsHttpHandler"/> von WebFetch (#580): erst
|
||||
/// auflösen und prüfen, dann mit genau den geprüften Adressen verbinden.
|
||||
/// </summary>
|
||||
public static async ValueTask<Stream> ConnectAsync(
|
||||
SocketsHttpConnectionContext context, CancellationToken ct)
|
||||
{
|
||||
var endpoint = context.DnsEndPoint;
|
||||
var addresses = await ResolveAllowedAsync(
|
||||
endpoint.Host, (host, token) => Dns.GetHostAddressesAsync(host, token), ct);
|
||||
|
||||
var socket = new Socket(SocketType.Stream, ProtocolType.Tcp) { NoDelay = true };
|
||||
|
||||
try
|
||||
{
|
||||
await socket.ConnectAsync(addresses, endpoint.Port, ct);
|
||||
return new NetworkStream(socket, ownsSocket: true);
|
||||
}
|
||||
catch
|
||||
{
|
||||
socket.Dispose();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Löst einen Namen auf und gibt die Adressen nur zurück, wenn <b>keine</b> davon im
|
||||
/// lokalen oder privaten Netz liegt.
|
||||
///
|
||||
/// <para>Alle und nicht „die erste erlaubte": Ein Name mit einer öffentlichen und einer
|
||||
/// internen Adresse ist genau das Werkzeug, mit dem man eine Prüfung überlistet, die nur
|
||||
/// eine Stichprobe nimmt.</para>
|
||||
/// </summary>
|
||||
/// <param name="resolve">Die Namensauflösung — austauschbar, damit sich die Sperre ohne
|
||||
/// Netz prüfen lässt.</param>
|
||||
public static async Task<IPAddress[]> ResolveAllowedAsync(
|
||||
string host,
|
||||
Func<string, CancellationToken, Task<IPAddress[]>> resolve,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var value = host.Trim().Trim('[', ']');
|
||||
|
||||
var addresses = IPAddress.TryParse(value, out var literal)
|
||||
? [literal]
|
||||
: await resolve(value, ct);
|
||||
|
||||
if (addresses.Length == 0)
|
||||
throw new HttpRequestException($"Adresse '{host}' ließ sich nicht auflösen.");
|
||||
|
||||
foreach (var address in addresses)
|
||||
{
|
||||
if (IsPrivateOrLocal(address))
|
||||
{
|
||||
throw new HttpRequestException(
|
||||
$"Adresse '{host}' löst auf {address} im lokalen oder privaten Netz auf und ist gesperrt.");
|
||||
}
|
||||
}
|
||||
|
||||
return addresses;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -295,7 +295,16 @@ public sealed class WebFetchTool : IAgentTool
|
||||
{
|
||||
// Weiterleitungen bewusst NICHT automatisch verfolgen — sie werden einzeln
|
||||
// aufgelöst und geprüft.
|
||||
var handler = new HttpClientHandler { AllowAutoRedirect = false };
|
||||
//
|
||||
// #580: Die Zieladresse wird beim Verbinden geprüft, nicht nur der Name in der URL.
|
||||
// Ohne Proxy, weil über einen Proxy der Proxy auflöst und verbindet — die Prüfung
|
||||
// sähe dann nur dessen Adresse, nie das eigentliche Ziel.
|
||||
var handler = new SocketsHttpHandler
|
||||
{
|
||||
AllowAutoRedirect = false,
|
||||
UseProxy = false,
|
||||
ConnectCallback = UrlGuard.ConnectAsync
|
||||
};
|
||||
var client = new HttpClient(handler, disposeHandler: true);
|
||||
var timeout = GetConfigInt(config, "timeoutSeconds", 15);
|
||||
client.Timeout = TimeSpan.FromSeconds(timeout);
|
||||
|
||||
Reference in New Issue
Block a user