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:
co-authored by
Claude Opus 4.8
parent
69b5704add
commit
e5067cae70
@@ -0,0 +1,95 @@
|
||||
using ClawdDotNet.Tools.DirectAPI;
|
||||
using Shouldly;
|
||||
|
||||
namespace ClawdDotNet.Tools.Tests.DirectAPI;
|
||||
|
||||
/// <summary>
|
||||
/// S3 aus der Bestandsaufnahme: Die abgerufene URL ging als "source" an das Modell —
|
||||
/// inklusive apikey= im Query-String. Der Schlüssel landete damit im Kontext, wurde
|
||||
/// bei jedem Folgeschritt erneut gesendet und in ChatContext.json geschrieben.
|
||||
/// </summary>
|
||||
public sealed class UrlSanitizerTests
|
||||
{
|
||||
private const string Secret = "GEHEIM-abc123XYZ";
|
||||
|
||||
[Theory]
|
||||
[InlineData("https://api.twelvedata.com/quote?symbol=NVDA&apikey=GEHEIM-abc123XYZ")]
|
||||
[InlineData("https://www.alphavantage.co/query?function=GLOBAL_QUOTE&symbol=X&apikey=GEHEIM-abc123XYZ")]
|
||||
[InlineData("https://api.example.com/v1?api_key=GEHEIM-abc123XYZ&x=1")]
|
||||
[InlineData("https://api.example.com/v1?token=GEHEIM-abc123XYZ")]
|
||||
[InlineData("https://api.example.com/v1?access_token=GEHEIM-abc123XYZ")]
|
||||
[InlineData("https://api.example.com/v1?KEY=GEHEIM-abc123XYZ")]
|
||||
[InlineData("https://api.example.com/v1?ApiKey=GEHEIM-abc123XYZ&other=2")]
|
||||
[InlineData("https://api.example.com/v1?secret=GEHEIM-abc123XYZ")]
|
||||
[InlineData("https://api.example.com/v1?a=1&apikey=GEHEIM-abc123XYZ#fragment")]
|
||||
public void Sensible_Parameter_werden_maskiert(string url)
|
||||
{
|
||||
var sanitized = UrlSanitizer.Sanitize(url);
|
||||
|
||||
sanitized.ShouldNotContain(Secret);
|
||||
sanitized.ShouldContain("***");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Harmlose_Parameter_bleiben_lesbar()
|
||||
{
|
||||
var sanitized = UrlSanitizer.Sanitize(
|
||||
"https://api.twelvedata.com/quote?symbol=NVDA&interval=1day&apikey=GEHEIM-abc123XYZ");
|
||||
|
||||
sanitized.ShouldContain("symbol=NVDA");
|
||||
sanitized.ShouldContain("interval=1day");
|
||||
sanitized.ShouldContain("apikey=***");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Der_Pfad_bleibt_erhalten()
|
||||
{
|
||||
var sanitized = UrlSanitizer.Sanitize("https://api.massive.com/v2/aggs/ticker/NVDA?apikey=x");
|
||||
|
||||
sanitized.ShouldStartWith("https://api.massive.com/v2/aggs/ticker/NVDA?");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ein_Fragment_bleibt_erhalten()
|
||||
{
|
||||
var sanitized = UrlSanitizer.Sanitize("https://x.de/a?apikey=geheim#abschnitt");
|
||||
|
||||
sanitized.ShouldEndWith("#abschnitt");
|
||||
sanitized.ShouldNotContain("geheim");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("https://api.example.com/v1")]
|
||||
[InlineData("https://api.example.com/v1?symbol=NVDA")]
|
||||
public void URLs_ohne_sensible_Parameter_bleiben_unveraendert(string url)
|
||||
{
|
||||
UrlSanitizer.Sanitize(url).ShouldBe(url);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(null)]
|
||||
[InlineData("")]
|
||||
[InlineData(" ")]
|
||||
public void Leere_Eingaben_fuehren_nicht_zu_einem_Fehler(string? url)
|
||||
{
|
||||
Should.NotThrow(() => UrlSanitizer.Sanitize(url));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ein_Parameter_ohne_Wert_stoert_nicht()
|
||||
{
|
||||
var sanitized = UrlSanitizer.Sanitize("https://x.de/a?flag&apikey=geheim");
|
||||
|
||||
sanitized.ShouldContain("flag");
|
||||
sanitized.ShouldNotContain("geheim");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ein_Schluessel_als_Teil_eines_anderen_Namens_wird_nicht_faelschlich_maskiert()
|
||||
{
|
||||
// "monkey" enthält "key", ist aber kein sensibler Parameter.
|
||||
var sanitized = UrlSanitizer.Sanitize("https://x.de/a?monkey=banane");
|
||||
|
||||
sanitized.ShouldBe("https://x.de/a?monkey=banane");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user