diff --git a/packages/yt-dlp/src/download.ts b/packages/yt-dlp/src/download.ts index 8724c74ab..887b3c7bb 100644 --- a/packages/yt-dlp/src/download.ts +++ b/packages/yt-dlp/src/download.ts @@ -52,6 +52,10 @@ export function isVideoUrl(value: string): boolean { * The container follows the target's extension. */ export async function downloadVideo(url: string, target: string, options: { readonly signal?: AbortSignal } = {}): Promise { + const link = url.trim(); + if (!isVideoUrl(link)) { + throw new Error(`Video link ${url} must be an http or https URL`); + } const container = extname(target).slice(1).toLowerCase(); if (!["mp4", "mkv", "webm", "mov"].includes(container)) { throw new Error(`${target} must end in .mp4, .mkv, .webm or .mov`); @@ -74,7 +78,9 @@ export async function downloadVideo(url: string, target: string, options: { read "--merge-output-format", container, "--format-sort", "res:1080,vcodec:h264", "--output", join(work, "video.%(ext)s"), - url, + // Terminate option parsing: a link-shaped argument must never re-enable an option + // (--plugin-dirs would run Python code from a caller-named directory). + "--", link, ], { timeout: 900_000, maxBuffer: 10 * 1024 * 1024, signal: options.signal }); } catch (cause) { if (options.signal?.aborted) throw cause; diff --git a/packages/yt-dlp/test/download.test.ts b/packages/yt-dlp/test/download.test.ts index 7d5c3db66..77ddc03e8 100644 --- a/packages/yt-dlp/test/download.test.ts +++ b/packages/yt-dlp/test/download.test.ts @@ -69,6 +69,7 @@ test("download resolves its locked runtime from an installed package outside the assert.match(command, /yt-dlp/u); assert.ok(args.includes("--no-remote-components")); assert.ok(args.includes("--ignore-config")); + assert.equal(args.at(-2), "--", "the link must sit behind the end-of-options separator"); assert.equal(args.at(-1), "https://example.test/video"); const output = args[args.indexOf("--output") + 1]!.replace("%(ext)s", "mp4"); writeFileSync(output, "downloaded bytes"); @@ -83,6 +84,17 @@ test("download resolves its locked runtime from an installed package outside the assert.equal(calls, 3); }); +test("download refuses option-shaped links before touching the environment", async () => { + await assert.rejects( + downloadVideo("--plugin-dirs=/tmp/evil", join(tmpdir(), "never.mp4")), + /http or https URL/u, + ); + await assert.rejects( + downloadVideo("file:///etc/passwd", join(tmpdir(), "never.mp4")), + /http or https URL/u, + ); +}); + test("the pinned yt-dlp project belongs to its behavior package", () => { const project = fileURLToPath(new URL("../runtime", import.meta.url)); assert.ok(existsSync(join(project, "pyproject.toml")));