using ClawdDotNet.Tools.SocialMediaManager; using Shouldly; namespace ClawdDotNet.Tools.Tests.SocialMedia; /// /// 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. /// 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"); } }