From 304f423146154bedaa59f9acefc9a993e7061a8f Mon Sep 17 00:00:00 2001 From: Paillat-dev Date: Mon, 11 May 2026 13:59:38 +0200 Subject: [PATCH 1/2] feat: Add sensible ffmpeg protocol whitelist and better docs --- discord/player.py | 103 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 102 insertions(+), 1 deletion(-) diff --git a/discord/player.py b/discord/player.py index f80bba9c1f..9d89eb1d7a 100644 --- a/discord/player.py +++ b/discord/player.py @@ -38,7 +38,7 @@ import time import warnings from math import floor -from typing import IO, TYPE_CHECKING, Any, Callable, Generic, TypeVar +from typing import IO, TYPE_CHECKING, Any, Callable, Generic, Literal, TypeVar, overload from .enums import SpeakingState from .errors import ClientException @@ -309,6 +309,11 @@ def cleanup(self) -> None: self._process = self._stdout = self._stdin = self._stderr = MISSING +DEFAULT_PROTOCOL_WHITELIST: set[str] = frozenset( + {"file", "http", "https", "tcp", "tls", "crypto", "pipe", "fd", "cache"} +) + + class FFmpegPCMAudio(FFmpegAudio): """An audio source from FFmpeg (or AVConv). @@ -325,6 +330,16 @@ class FFmpegPCMAudio(FFmpegAudio): The input that ffmpeg will take and convert to PCM bytes. If ``pipe`` is ``True`` then this is a file-like object that is passed to the stdin of ffmpeg. + + .. warning:: + + The ``source`` parameter is passed directly to your executable's ``-i`` flag and + interpreted through its full protocol machinery. This means it can accept + ``file://`` paths (local file read), ``http(s)://`` to internal services + (SSRF), ``concat:`` (multi-file read), and other dangerous schemes. + You should never pass attacker-controlled values (e.g. raw user input from a chat + command) to ``source`` without validation. + executable: :class:`str` The executable name (and path) to use. Defaults to ``ffmpeg``. @@ -342,6 +357,12 @@ class FFmpegPCMAudio(FFmpegAudio): Extra command line arguments to pass to ffmpeg before the ``-i`` flag. options: Optional[:class:`str`] Extra command line arguments to pass to ffmpeg after the ``-i`` flag. + protocol_whitelist: Optional[:class:`str`] + A comma-separated list of protocols that ffmpeg is allowed to use. + Defaults to ``"file,http,https,tcp,tls,crypto,pipe,fd,cache"``, which + blocks dangerous schemes such as ``concat:``, ``subfile:``, ``data:``, + and ``gopher:``. Set to ``None`` to disable the whitelist entirely + (not recommended unless you are certain of the input source). Raises ------ @@ -349,6 +370,32 @@ class FFmpegPCMAudio(FFmpegAudio): The subprocess failed to be created. """ + @overload + def __init__( + self, + source: io.BufferedIOBase, + *, + executable: str = ..., + pipe: Literal[True] = ..., + stderr: IO[bytes] | None = ..., + before_options: str | None = ..., + options: str | None = ..., + protocol_whitelist: set[str] | None = ..., + ) -> None: ... + + @overload + def __init__( + self, + source: str, + *, + executable: str = ..., + pipe: Literal[False] = ..., + stderr: IO[bytes] | None = ..., + before_options: str | None = ..., + options: str | None = ..., + protocol_whitelist: set[str] | None = ..., + ) -> None: ... + def __init__( self, source: str | io.BufferedIOBase, @@ -358,6 +405,7 @@ def __init__( stderr: IO[bytes] | None = None, before_options: str | None = None, options: str | None = None, + protocol_whitelist: set[str] | None = DEFAULT_PROTOCOL_WHITELIST, ) -> None: args = [] subprocess_kwargs = { @@ -368,6 +416,9 @@ def __init__( if isinstance(before_options, str): args.extend(shlex.split(before_options)) + if protocol_whitelist is not None: + args.extend(["-protocol_whitelist", ",".join(protocol_whitelist)]) + args.append("-i") args.append("-" if pipe else source) @@ -430,6 +481,16 @@ class FFmpegOpusAudio(FFmpegAudio): The input that ffmpeg will take and convert to Opus bytes. If ``pipe`` is ``True`` then this is a file-like object that is passed to the stdin of ffmpeg. + + .. warning:: + + The ``source`` parameter is passed directly to your executable's ``-i`` flag and + interpreted through its full protocol machinery. This means it can accept + ``file://`` paths (local file read), ``http(s)://`` to internal services + (SSRF), ``concat:`` (multi-file read), and other dangerous schemes. + You should never pass attacker-controlled values (e.g. raw user input from a chat + command) to ``source`` without validation. + bitrate: :class:`int` The bitrate in kbps to encode the output to. Defaults to ``128``. codec: Optional[:class:`str`] @@ -456,6 +517,12 @@ class FFmpegOpusAudio(FFmpegAudio): Extra command line arguments to pass to ffmpeg before the ``-i`` flag. options: Optional[:class:`str`] Extra command line arguments to pass to ffmpeg after the ``-i`` flag. + protocol_whitelist: Optional[:class:`set[str]`] + A set of protocols that ffmpeg is allowed to use. + Defaults to ``{"file", "http", "https", "tcp", "tls", "crypto", "pipe", "fd", "cache"}``, which + blocks dangerous schemes such as ``concat:``, ``subfile:``, ``data:``, + and ``gopher:``. Set to ``None`` to disable the whitelist entirely + (not recommended unless you are certain of the input source). Raises ------ @@ -463,6 +530,36 @@ class FFmpegOpusAudio(FFmpegAudio): The subprocess failed to be created. """ + @overload + def __init__( + self, + source: io.BufferedIOBase, + *, + bitrate: int | None = None, + codec: str | None = None, + executable: str = ..., + pipe: Literal[True] = ..., + stderr: IO[bytes] | None = ..., + before_options: str | None = ..., + options: str | None = ..., + protocol_whitelist: set[str] | None = ..., + ) -> None: ... + + @overload + def __init__( + self, + source: str, + *, + bitrate: int | None = None, + codec: str | None = None, + executable: str = ..., + pipe: Literal[False] = ..., + stderr: IO[bytes] | None = ..., + before_options: str | None = ..., + options: str | None = ..., + protocol_whitelist: set[str] | None = ..., + ) -> None: ... + def __init__( self, source: str | io.BufferedIOBase, @@ -474,6 +571,7 @@ def __init__( stderr: IO[bytes] | None = None, before_options: str | None = None, options: str | None = None, + protocol_whitelist: set[str] | None = DEFAULT_PROTOCOL_WHITELIST, ) -> None: args = [] subprocess_kwargs = { @@ -484,6 +582,9 @@ def __init__( if isinstance(before_options, str): args.extend(shlex.split(before_options)) + if protocol_whitelist is not None: + args.extend(["-protocol_whitelist", ",".join(protocol_whitelist)]) + args.append("-i") args.append("-" if pipe else source) From 2cc8bdf880cec071812aace2519e5236f4091575 Mon Sep 17 00:00:00 2001 From: Paillat Date: Mon, 11 May 2026 14:45:55 +0200 Subject: [PATCH 2/2] Update discord/player.py Co-authored-by: Michael Signed-off-by: Paillat --- discord/player.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/player.py b/discord/player.py index 9d89eb1d7a..250079f4f0 100644 --- a/discord/player.py +++ b/discord/player.py @@ -357,7 +357,7 @@ class FFmpegPCMAudio(FFmpegAudio): Extra command line arguments to pass to ffmpeg before the ``-i`` flag. options: Optional[:class:`str`] Extra command line arguments to pass to ffmpeg after the ``-i`` flag. - protocol_whitelist: Optional[:class:`str`] + protocol_whitelist: Optional[:class:`set[str]`] A comma-separated list of protocols that ffmpeg is allowed to use. Defaults to ``"file,http,https,tcp,tls,crypto,pipe,fd,cache"``, which blocks dangerous schemes such as ``concat:``, ``subfile:``, ``data:``,