Sicherheitsluecken S1, S2 und S3 schliessen

Neues Testprojekt tests/ClawdDotNet.Tools.Tests. Die Angriffsfaelle aus der
Bestandsaufnahme bleiben darin dauerhaft als Testfaelle dokumentiert — zusammen
mit Gegenproben, damit die Fixes nicht zu streng werden und legitime Nutzung
blockieren.

S3 — DirectAPI gab die abgerufene URL als Quelle an das Modell zurueck, samt
API-Schluessel im Query-String. Der Schluessel landete damit im
Konversationskontext, wurde bei jedem Folgeschritt erneut gesendet, in
ChatContext.json geschrieben und in die Logs uebernommen. UrlSanitizer maskiert
sensible Query-Parameter; auch die Fehlermeldungen sind betroffen und werden
bereinigt.

S2 — Die Kanal-/Video-Angabe wurde ungeprueft in eine Argument-Zeichenkette fuer
yt-dlp interpoliert. UseShellExecute=false verhindert Shell-Metazeichen, nicht
aber Options-Injection: yt-dlp kennt die Option --exec, die beliebige Befehle
ausfuehrt. Kritisch, weil der Agent untrusted Inhalte verarbeitet — eine
Prompt-Injection darin konnte ihn dazu bringen, genau so einen Wert zu setzen.

YouTubeUrl validiert Handles und URLs gegen die zulaessigen YouTube-Hosts und
lehnt alles ab, was mit einem Bindestrich beginnt. Die Argumente gehen jetzt
einzeln ueber ProcessStartInfo.ArgumentList, die Adresse steht hinter dem
Optionsende-Trenner. Der ffmpeg-Aufruf wurde ebenso umgestellt.

S1 — Die Tabellen-Whitelist suchte den erlaubten Namen als Teilzeichenkette
irgendwo im Statement, auch in Kommentaren. Bei einer Freigabe fuer prices
genuegte deshalb ein DELETE auf users mit einem Kommentar, der prices enthielt,
um eine beliebige Tabelle zu loeschen. Umgekehrt galten harmlose Abfragen, die
ein Schluesselwort nur als Wert enthielten, faelschlich als Schreibzugriff.

SqlGuard entfernt zuerst Kommentare und String-Literale, lehnt mehrere Statements
ab, bestimmt die Operation am ersten Schluesselwort und extrahiert Tabellennamen
gezielt hinter FROM/JOIN/INTO/UPDATE/TABLE — inklusive kommagetrennter Listen mit
Aliassen. JEDE referenzierte Tabelle muss freigegeben sein, nicht irgendeine.
Ohne Whitelist wird nichts durchgelassen. Die MongoDB-Pruefung vergleicht den
Collection-Namen jetzt exakt statt per Teilzeichenkette.

178 Tests gruen (91 Core, 87 Tools).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Richard
2026-07-27 18:49:17 +02:00
co-authored by Claude Opus 4.8
parent 69b5704add
commit e5067cae70
13 changed files with 973 additions and 72 deletions
+30 -34
View File
@@ -95,22 +95,22 @@ public sealed class DatabaseTool : IAgentTool
var accessLevel = GetAccessLevel(context);
// Sicherheitsprüfungen
if (IsAdminAttempt(sql))
{
if (accessLevel != DatabaseAccessLevel.Admin)
return ToolResult.Fail("Sicherheitsfehler: Strukturänderungen (DDL) sind für diesen Agenten nicht erlaubt.");
}
else if (IsWriteAttempt(sql))
{
if (accessLevel == DatabaseAccessLevel.ReadOnly)
return ToolResult.Fail("Sicherheitsfehler: Schreibzugriff ist für diesen Agenten deaktiviert (ReadOnly).");
}
// Statement analysieren: Kommentare, Mehrfach-Statements und String-Literale
// werden dabei behandelt, bevor Operation und Tabellen bestimmt werden.
var inspection = SqlGuard.Inspect(sql, GetAllowedTables(context));
// Tabellen-Whitelist-Prüfung
if (!IsTableAllowed(sql, context))
if (!inspection.IsValid)
return ToolResult.Fail($"Sicherheitsfehler: {inspection.Error}");
switch (inspection.Operation)
{
return ToolResult.Fail("Sicherheitsfehler: Zugriff auf eine oder mehrere Tabellen im Statement ist nicht erlaubt.");
case SqlOperation.Schema when accessLevel != DatabaseAccessLevel.Admin:
return ToolResult.Fail(
"Sicherheitsfehler: Strukturänderungen (DDL) sind für diesen Agenten nicht erlaubt.");
case SqlOperation.Write when accessLevel == DatabaseAccessLevel.ReadOnly:
return ToolResult.Fail(
"Sicherheitsfehler: Schreibzugriff ist für diesen Agenten deaktiviert (ReadOnly).");
}
using DbConnection conn = type switch
@@ -152,7 +152,7 @@ public sealed class DatabaseTool : IAgentTool
var collectionName = input.TryGetProperty("collection", out var c) ? c.GetString() : null;
if (string.IsNullOrWhiteSpace(collectionName)) return ToolResult.Fail("'collection' ist erforderlich.");
if (!IsTableAllowed(collectionName, context))
if (!IsCollectionAllowed(collectionName, context))
{
return ToolResult.Fail($"Sicherheitsfehler: Zugriff auf Collection '{collectionName}' ist nicht erlaubt.");
}
@@ -186,18 +186,6 @@ public sealed class DatabaseTool : IAgentTool
}
}
private bool IsWriteAttempt(string sql)
{
var lower = sql.ToLowerInvariant();
return lower.Contains("insert") || lower.Contains("update") || lower.Contains("delete");
}
private bool IsAdminAttempt(string sql)
{
var lower = sql.ToLowerInvariant();
return lower.Contains("drop") || lower.Contains("alter") || lower.Contains("create") || lower.Contains("truncate");
}
private DatabaseAccessLevel GetAccessLevel(AgentToolContext context)
{
if (context.ToolConfig.TryGetValue("accessLevel", out var val) && val != null)
@@ -215,16 +203,24 @@ public sealed class DatabaseTool : IAgentTool
return DatabaseAccessLevel.ReadOnly;
}
private bool IsTableAllowed(string input, AgentToolContext context)
internal static List<string> GetAllowedTables(AgentToolContext context)
{
if (!context.ToolConfig.TryGetValue("allowedTables", out var val) || val is not JsonElement je)
if (!context.ToolConfig.TryGetValue("allowedTables", out var val) || val is not JsonElement je
|| je.ValueKind != JsonValueKind.Array)
{
return false;
return [];
}
var allowed = je.EnumerateArray().Select(x => x.GetString()?.ToLowerInvariant()).ToList();
var inputLower = input.ToLowerInvariant();
return allowed.Any(t => t != null && inputLower.Contains(t));
return je.EnumerateArray()
.Select(x => x.GetString())
.Where(s => !string.IsNullOrWhiteSpace(s))
.ToList()!;
}
/// <summary>
/// MongoDB kennt kein SQL — hier genügt der exakte Vergleich des Collection-Namens.
/// Bisher wurde auch hier per Teilzeichenkette geprüft.
/// </summary>
private static bool IsCollectionAllowed(string collection, AgentToolContext context)
=> GetAllowedTables(context).Contains(collection, StringComparer.OrdinalIgnoreCase);
}