security: fix #2 — wire hardened mpv options into _MpvGLWidget

Replaces the inline mpv.MPV(...) literal kwargs with a call through
build_mpv_kwargs(), which adds ytdl=no, load_scripts=no, a lavf
protocol whitelist (file,http,https,tls,tcp), and POSIX input_conf
lockdown. Closes the yt-dlp delegation surface (CVE-prone extractors
invoked on attacker-supplied URLs) and the concat:/subfile: local-
file-read gadget via ffmpeg's lavf demuxer.

behavior change from v0.2.5: any file_url whose host is only
handled by yt-dlp (youtube.com, reddit.com, etc.) will no longer
play. Boorus do not legitimately return such URLs, so in practice
this only affects hostile responses. Cached local files and direct
https .mp4/.webm/.mkv continue to work.

Manually smoke tested: played a cached local .mp4 from the library
(file: protocol) and a fresh network .webm from a danbooru search
(https: protocol) — both work.

Audit-Ref: SECURITY_AUDIT.md finding #2
Severity: High
This commit is contained in:
pax 2026-04-11 16:07:33 -05:00
parent 22744c48af
commit 72803f0b14

View File

@ -3,6 +3,7 @@
from __future__ import annotations from __future__ import annotations
import logging import logging
import sys
from PySide6.QtCore import Signal from PySide6.QtCore import Signal
from PySide6.QtOpenGLWidgets import QOpenGLWidget as _QOpenGLWidget from PySide6.QtOpenGLWidgets import QOpenGLWidget as _QOpenGLWidget
@ -10,6 +11,8 @@ from PySide6.QtWidgets import QWidget, QVBoxLayout
import mpv as mpvlib import mpv as mpvlib
from ._mpv_options import build_mpv_kwargs
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -35,57 +38,14 @@ class _MpvGLWidget(QWidget):
self._frame_ready.connect(self._gl.update) self._frame_ready.connect(self._gl.update)
# Create mpv eagerly on the main thread. # Create mpv eagerly on the main thread.
# #
# `ao=pulse` is critical for Linux Discord screen-share audio # Options come from `build_mpv_kwargs` (see `_mpv_options.py`
# capture. Discord on Linux only enumerates audio clients via # for the full rationale). Summary: Discord screen-share audio
# the libpulse API; it does not see clients that talk to # fix via `ao=pulse`, fast-load vd-lavc options, network cache
# PipeWire natively (which is mpv's default `ao=pipewire`). # tuning for the uncached-video fast path, and the SECURITY
# Forcing the pulseaudio output here makes mpv go through # hardening from audit #2 (ytdl=no, load_scripts=no,
# PipeWire's pulseaudio compatibility layer, which Discord # demuxer_lavf_o protocol whitelist, POSIX input_conf null).
# picks up the same way it picks up Firefox. Without this,
# videos play locally but the audio is silently dropped from
# any Discord screen share. See:
# https://github.com/mpv-player/mpv/issues/11100
# https://github.com/edisionnano/Screenshare-with-audio-on-Discord-with-Linux
# On Windows mpv ignores `ao=pulse` and falls through to the
# next entry, so listing `wasapi` second keeps Windows playback
# working without a platform branch here.
#
# `audio_client_name` is the name mpv registers with the audio
# backend. Sets `application.name` and friends so capture tools
# group mpv's audio under the booru-viewer app identity instead
# of the default "mpv Media Player".
self._mpv = mpvlib.MPV( self._mpv = mpvlib.MPV(
vo="libmpv", **build_mpv_kwargs(is_windows=sys.platform == "win32"),
hwdec="auto",
keep_open="yes",
ao="pulse,wasapi,",
audio_client_name="booru-viewer",
input_default_bindings=False,
input_vo_keyboard=False,
osc=False,
# Fast-load options: shave ~50-100ms off first-frame decode
# for h264/hevc by skipping a few bitstream-correctness checks
# (`vd-lavc-fast`) and the in-loop filter on non-keyframes
# (`vd-lavc-skiploopfilter=nonkey`). The artifacts are only
# visible on the first few frames before the decoder steady-
# state catches up, and only on degraded sources. mpv
# documents these as safe for "fast load" use cases like
# ours where we want the first frame on screen ASAP and
# don't care about a tiny quality dip during ramp-up.
vd_lavc_fast="yes",
vd_lavc_skiploopfilter="nonkey",
# Network streaming tuning for the uncached-video fast path.
# cache=yes is mpv's default for network sources but explicit
# is clearer. cache_pause=no keeps playback running through
# brief buffer underruns instead of pausing — for short booru
# clips a momentary stutter beats a pause icon. demuxer caps
# keep RAM bounded. network_timeout=10 replaces mpv's ~60s
# default so stalled connections surface errors promptly.
cache="yes",
cache_pause="no",
demuxer_max_bytes="50MiB",
demuxer_readahead_secs="20",
network_timeout="10",
) )
# Wire up the GL surface's callbacks to us # Wire up the GL surface's callbacks to us
self._gl._owner = self self._gl._owner = self