Files
ClawdDotNet/tests/ClawdDotNet.Tools.Tests/SocialMedia/YouTubeUrlTests.cs
T
RichardandClaude Opus 4.8 e5067cae70 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>
2026-07-27 18:49:17 +02:00

140 lines
6.0 KiB
C#

using ClawdDotNet.Tools.SocialMediaManager;
using Shouldly;
namespace ClawdDotNet.Tools.Tests.SocialMedia;
/// <summary>
/// S2 aus der Bestandsaufnahme: Die Kanal-/Video-Angabe wurde ungeprüft in eine
/// Argument-Zeichenkette für yt-dlp interpoliert. UseShellExecute=false verhindert
/// Shell-Metazeichen, nicht aber Options-Injection — yt-dlp kennt --exec, das beliebige
/// Befehle ausführt.
///
/// Die Angriffsfälle bleiben hier dauerhaft als Testfälle dokumentiert.
/// </summary>
public sealed class YouTubeUrlTests
{
// ═══════════════════════════════════════════════════════════
// Angriffe
// ═══════════════════════════════════════════════════════════
[Theory]
[InlineData("--exec cmd.exe https://www.youtube.com/@kanal")]
[InlineData("--exec=calc.exe")]
[InlineData("-o /tmp/evil")]
[InlineData("--config-location /tmp/evil.conf")]
[InlineData("--paths /windows/system32")]
[InlineData("-")]
public void Optionsartige_Eingaben_werden_abgelehnt(string input)
{
YouTubeUrl.TryResolveChannelUrl(input, out _, out var error).ShouldBeFalse();
error.ShouldNotBeNull();
}
[Theory]
[InlineData("https://evil.com/video")]
[InlineData("https://youtube.com.attacker.net/@kanal")]
[InlineData("https://notyoutube.com/@kanal")]
[InlineData("http://192.168.178.10:8418/Richard/ClawdDotNet.git")]
[InlineData("file:///C:/Windows/win.ini")]
[InlineData("ftp://example.com/datei")]
public void Fremde_Hosts_und_Schemata_werden_abgelehnt(string url)
{
YouTubeUrl.TryResolveChannelUrl(url, out _, out var error).ShouldBeFalse();
error.ShouldNotBeNull();
}
[Theory]
[InlineData("kanal name")] // Leerzeichen
[InlineData("kanal\"name")] // Anführungszeichen
[InlineData("kanal\nname")] // Zeilenumbruch
[InlineData("kanal;name")]
[InlineData("kanal&name")]
[InlineData("../../etc/passwd")]
[InlineData("@kanal/../../evil")]
public void Unsaubere_Handles_werden_abgelehnt(string input)
{
YouTubeUrl.TryResolveChannelUrl(input, out _, out var error).ShouldBeFalse();
error.ShouldNotBeNull();
}
[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData(" ")]
public void Leere_Eingaben_werden_abgelehnt(string? input)
{
YouTubeUrl.TryResolveChannelUrl(input, out _, out _).ShouldBeFalse();
}
// ═══════════════════════════════════════════════════════════
// Gegenproben — legitime Eingaben müssen weiter funktionieren
// ═══════════════════════════════════════════════════════════
[Theory]
[InlineData("@Computerphile", "https://www.youtube.com/@Computerphile/videos")]
[InlineData("Computerphile", "https://www.youtube.com/@Computerphile/videos")]
[InlineData("kanal_mit-punkt.name", "https://www.youtube.com/@kanal_mit-punkt.name/videos")]
public void Handles_werden_zu_Kanal_URLs(string input, string expected)
{
YouTubeUrl.TryResolveChannelUrl(input, out var url, out var error).ShouldBeTrue(error);
url.ShouldBe(expected);
}
[Theory]
[InlineData("https://www.youtube.com/watch?v=dQw4w9WgXcQ")]
[InlineData("https://youtube.com/@kanal/videos")]
[InlineData("https://m.youtube.com/watch?v=abc")]
[InlineData("https://music.youtube.com/watch?v=abc")]
[InlineData("https://youtu.be/dQw4w9WgXcQ")]
public void Echte_YouTube_URLs_werden_durchgelassen(string url)
{
YouTubeUrl.TryResolveChannelUrl(url, out var resolved, out var error).ShouldBeTrue(error);
resolved.ShouldBe(url);
}
// ═══════════════════════════════════════════════════════════
// Argumentbildung
// ═══════════════════════════════════════════════════════════
[Fact]
public void Die_URL_steht_hinter_dem_Optionsende_Trenner()
{
// "--" beendet die Optionsliste. Selbst wenn ein Wert wie eine Option aussähe,
// würde yt-dlp ihn danach als Adresse behandeln.
var args = YouTubeUrl.BuildLatestVideoIdArgs("https://www.youtube.com/@kanal/videos");
var separator = args.IndexOf("--");
separator.ShouldBeGreaterThanOrEqualTo(0);
args[^1].ShouldBe("https://www.youtube.com/@kanal/videos");
args.IndexOf(args[^1]).ShouldBeGreaterThan(separator);
}
[Fact]
public void Jedes_Argument_ist_ein_eigener_Eintrag()
{
// Entscheidend: Kein Eintrag darf mehrere Argumente enthalten — sonst könnten
// sie beim Start wieder aufgetrennt werden.
var args = YouTubeUrl.BuildAudioDownloadArgs(
@"C:\Pfad mit Leerzeichen\datei.mp3",
"https://www.youtube.com/watch?v=abc",
@"C:\ffmpeg\bin",
convertToMp3: true);
args.ShouldContain(@"C:\Pfad mit Leerzeichen\datei.mp3");
args.ShouldContain(@"C:\ffmpeg\bin");
args.ShouldContain("--audio-format");
args.ShouldContain("mp3");
args[^1].ShouldBe("https://www.youtube.com/watch?v=abc");
}
[Fact]
public void Ohne_ffmpeg_entfaellt_die_Konvertierung()
{
var args = YouTubeUrl.BuildAudioDownloadArgs(
"/tmp/x.%(ext)s", "https://youtu.be/abc", null, convertToMp3: false);
args.ShouldNotContain("--audio-format");
args.ShouldNotContain("--ffmpeg-location");
}
}