Skip to content

Commit 304f423

Browse files
committed
feat: Add sensible ffmpeg protocol whitelist and better docs
1 parent 9eba65b commit 304f423

1 file changed

Lines changed: 102 additions & 1 deletion

File tree

discord/player.py

Lines changed: 102 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
import time
3939
import warnings
4040
from math import floor
41-
from typing import IO, TYPE_CHECKING, Any, Callable, Generic, TypeVar
41+
from typing import IO, TYPE_CHECKING, Any, Callable, Generic, Literal, TypeVar, overload
4242

4343
from .enums import SpeakingState
4444
from .errors import ClientException
@@ -309,6 +309,11 @@ def cleanup(self) -> None:
309309
self._process = self._stdout = self._stdin = self._stderr = MISSING
310310

311311

312+
DEFAULT_PROTOCOL_WHITELIST: set[str] = frozenset(
313+
{"file", "http", "https", "tcp", "tls", "crypto", "pipe", "fd", "cache"}
314+
)
315+
316+
312317
class FFmpegPCMAudio(FFmpegAudio):
313318
"""An audio source from FFmpeg (or AVConv).
314319
@@ -325,6 +330,16 @@ class FFmpegPCMAudio(FFmpegAudio):
325330
The input that ffmpeg will take and convert to PCM bytes.
326331
If ``pipe`` is ``True`` then this is a file-like object that is
327332
passed to the stdin of ffmpeg.
333+
334+
.. warning::
335+
336+
The ``source`` parameter is passed directly to your executable's ``-i`` flag and
337+
interpreted through its full protocol machinery. This means it can accept
338+
``file://`` paths (local file read), ``http(s)://`` to internal services
339+
(SSRF), ``concat:`` (multi-file read), and other dangerous schemes.
340+
You should never pass attacker-controlled values (e.g. raw user input from a chat
341+
command) to ``source`` without validation.
342+
328343
executable: :class:`str`
329344
The executable name (and path) to use. Defaults to ``ffmpeg``.
330345
@@ -342,13 +357,45 @@ class FFmpegPCMAudio(FFmpegAudio):
342357
Extra command line arguments to pass to ffmpeg before the ``-i`` flag.
343358
options: Optional[:class:`str`]
344359
Extra command line arguments to pass to ffmpeg after the ``-i`` flag.
360+
protocol_whitelist: Optional[:class:`str`]
361+
A comma-separated list of protocols that ffmpeg is allowed to use.
362+
Defaults to ``"file,http,https,tcp,tls,crypto,pipe,fd,cache"``, which
363+
blocks dangerous schemes such as ``concat:``, ``subfile:``, ``data:``,
364+
and ``gopher:``. Set to ``None`` to disable the whitelist entirely
365+
(not recommended unless you are certain of the input source).
345366
346367
Raises
347368
------
348369
ClientException
349370
The subprocess failed to be created.
350371
"""
351372

373+
@overload
374+
def __init__(
375+
self,
376+
source: io.BufferedIOBase,
377+
*,
378+
executable: str = ...,
379+
pipe: Literal[True] = ...,
380+
stderr: IO[bytes] | None = ...,
381+
before_options: str | None = ...,
382+
options: str | None = ...,
383+
protocol_whitelist: set[str] | None = ...,
384+
) -> None: ...
385+
386+
@overload
387+
def __init__(
388+
self,
389+
source: str,
390+
*,
391+
executable: str = ...,
392+
pipe: Literal[False] = ...,
393+
stderr: IO[bytes] | None = ...,
394+
before_options: str | None = ...,
395+
options: str | None = ...,
396+
protocol_whitelist: set[str] | None = ...,
397+
) -> None: ...
398+
352399
def __init__(
353400
self,
354401
source: str | io.BufferedIOBase,
@@ -358,6 +405,7 @@ def __init__(
358405
stderr: IO[bytes] | None = None,
359406
before_options: str | None = None,
360407
options: str | None = None,
408+
protocol_whitelist: set[str] | None = DEFAULT_PROTOCOL_WHITELIST,
361409
) -> None:
362410
args = []
363411
subprocess_kwargs = {
@@ -368,6 +416,9 @@ def __init__(
368416
if isinstance(before_options, str):
369417
args.extend(shlex.split(before_options))
370418

419+
if protocol_whitelist is not None:
420+
args.extend(["-protocol_whitelist", ",".join(protocol_whitelist)])
421+
371422
args.append("-i")
372423
args.append("-" if pipe else source)
373424

@@ -430,6 +481,16 @@ class FFmpegOpusAudio(FFmpegAudio):
430481
The input that ffmpeg will take and convert to Opus bytes.
431482
If ``pipe`` is ``True`` then this is a file-like object that is
432483
passed to the stdin of ffmpeg.
484+
485+
.. warning::
486+
487+
The ``source`` parameter is passed directly to your executable's ``-i`` flag and
488+
interpreted through its full protocol machinery. This means it can accept
489+
``file://`` paths (local file read), ``http(s)://`` to internal services
490+
(SSRF), ``concat:`` (multi-file read), and other dangerous schemes.
491+
You should never pass attacker-controlled values (e.g. raw user input from a chat
492+
command) to ``source`` without validation.
493+
433494
bitrate: :class:`int`
434495
The bitrate in kbps to encode the output to. Defaults to ``128``.
435496
codec: Optional[:class:`str`]
@@ -456,13 +517,49 @@ class FFmpegOpusAudio(FFmpegAudio):
456517
Extra command line arguments to pass to ffmpeg before the ``-i`` flag.
457518
options: Optional[:class:`str`]
458519
Extra command line arguments to pass to ffmpeg after the ``-i`` flag.
520+
protocol_whitelist: Optional[:class:`set[str]`]
521+
A set of protocols that ffmpeg is allowed to use.
522+
Defaults to ``{"file", "http", "https", "tcp", "tls", "crypto", "pipe", "fd", "cache"}``, which
523+
blocks dangerous schemes such as ``concat:``, ``subfile:``, ``data:``,
524+
and ``gopher:``. Set to ``None`` to disable the whitelist entirely
525+
(not recommended unless you are certain of the input source).
459526
460527
Raises
461528
------
462529
ClientException
463530
The subprocess failed to be created.
464531
"""
465532

533+
@overload
534+
def __init__(
535+
self,
536+
source: io.BufferedIOBase,
537+
*,
538+
bitrate: int | None = None,
539+
codec: str | None = None,
540+
executable: str = ...,
541+
pipe: Literal[True] = ...,
542+
stderr: IO[bytes] | None = ...,
543+
before_options: str | None = ...,
544+
options: str | None = ...,
545+
protocol_whitelist: set[str] | None = ...,
546+
) -> None: ...
547+
548+
@overload
549+
def __init__(
550+
self,
551+
source: str,
552+
*,
553+
bitrate: int | None = None,
554+
codec: str | None = None,
555+
executable: str = ...,
556+
pipe: Literal[False] = ...,
557+
stderr: IO[bytes] | None = ...,
558+
before_options: str | None = ...,
559+
options: str | None = ...,
560+
protocol_whitelist: set[str] | None = ...,
561+
) -> None: ...
562+
466563
def __init__(
467564
self,
468565
source: str | io.BufferedIOBase,
@@ -474,6 +571,7 @@ def __init__(
474571
stderr: IO[bytes] | None = None,
475572
before_options: str | None = None,
476573
options: str | None = None,
574+
protocol_whitelist: set[str] | None = DEFAULT_PROTOCOL_WHITELIST,
477575
) -> None:
478576
args = []
479577
subprocess_kwargs = {
@@ -484,6 +582,9 @@ def __init__(
484582
if isinstance(before_options, str):
485583
args.extend(shlex.split(before_options))
486584

585+
if protocol_whitelist is not None:
586+
args.extend(["-protocol_whitelist", ",".join(protocol_whitelist)])
587+
487588
args.append("-i")
488589
args.append("-" if pipe else source)
489590

0 commit comments

Comments
 (0)