Mit Roslyn umbenannt (Verweise folgen, Zeichenketten bleiben unberuehrt). Nicht umbenannt, weil gespeichert oder ueber die API gebunden (Stufe 4e): Properties, Record-Parameter, Enum-Member sowie die Parameter der Endpunkt-Handler (Routen-/Query-Bindung ueber den Namen). Die JSON-Felder anonymer Objekte bleiben ausdruecklich benannt (JsonFilter: pfad). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
140 lines
5.9 KiB
C#
140 lines
5.9 KiB
C#
using ClawdDotNet.Tools.SocialMediaManager;
|
|
using Shouldly;
|
|
|
|
namespace ClawdDotNet.Tools.Tests.SocialMedia;
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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 Option_like_inputs_are_rejected(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 Foreign_hosts_and_schemes_are_rejected(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 Unclean_handles_are_rejected(string input)
|
|
{
|
|
YouTubeUrl.TryResolveChannelUrl(input, out _, out var error).ShouldBeFalse();
|
|
error.ShouldNotBeNull();
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(null)]
|
|
[InlineData("")]
|
|
[InlineData(" ")]
|
|
public void Empty_inputs_are_rejected(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_become_channel_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 Real_YouTube_URLs_are_let_through(string url)
|
|
{
|
|
YouTubeUrl.TryResolveChannelUrl(url, out var resolved, out var error).ShouldBeTrue(error);
|
|
resolved.ShouldBe(url);
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════
|
|
// Building the arguments
|
|
// ═══════════════════════════════════════════════════════════
|
|
|
|
[Fact]
|
|
public void The_URL_comes_after_the_end_of_options_separator()
|
|
{
|
|
// "--" 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 Every_argument_is_its_own_entry()
|
|
{
|
|
// 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 Without_ffmpeg_the_conversion_is_skipped()
|
|
{
|
|
var args = YouTubeUrl.BuildAudioDownloadArgs(
|
|
"/tmp/x.%(ext)s", "https://youtu.be/abc", null, convertToMp3: false);
|
|
|
|
args.ShouldNotContain("--audio-format");
|
|
args.ShouldNotContain("--ffmpeg-location");
|
|
}
|
|
}
|