Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion packages/yt-dlp/src/download.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> {
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`);
Expand All @@ -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;
Expand Down
12 changes: 12 additions & 0 deletions packages/yt-dlp/test/download.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand All @@ -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")));
Expand Down