using ClawdDotNet.Tools.SocialMediaManager; using Shouldly; namespace ClawdDotNet.Tools.Tests.SocialMedia; /// /// S2 from the inventory: the channel/video statement was interpolated unchecked into an /// argument string for yt-dlp. UseShellExecute=false prevents /// shell metacharacters, but not option injection — yt-dlp knows --exec, which executes arbitrary /// commands. /// /// The attack cases stay permanently documented here as test cases. /// public sealed class YouTubeUrlTests { // ═══════════════════════════════════════════════════════════ // Attacks // ═══════════════════════════════════════════════════════════ [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")] // space [InlineData("kanal\"name")] // quotation marks [InlineData("kanal\nname")] // line break [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(); } // ═══════════════════════════════════════════════════════════ // Counter-checks — legitimate inputs must keep working // ═══════════════════════════════════════════════════════════ [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); } // ═══════════════════════════════════════════════════════════ // Building the arguments // ═══════════════════════════════════════════════════════════ [Fact] public void Die_URL_steht_hinter_dem_Optionsende_Trenner() { // "--" ends the option list. Even if a value looked like an option, // yt-dlp would treat it as an address after that. 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() { // Crucial: no entry may contain several arguments — otherwise they could // be split apart again at startup. 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"); } }