booru-viewer/tests/gui/media/test_mpv_options.py
pax 4db7943ac7 security: fix #2 — apply lavf protocol whitelist via property API
The previous attempt set ``demuxer_lavf_o`` as an init kwarg with a
comma-laden ``protocol_whitelist=file,http,https,tls,tcp`` value.
mpv rejected it with -7 OPT_FORMAT because python-mpv's init path
goes through ``mpv_set_option_string``, which routes through mpv's
keyvalue list parser — that parser splits on ``,`` to find entries,
shredding the protocol list into orphan tokens. Backslash-escaping
``\,`` did not unescape on this code path either.

Splits the option set into two helpers:

- ``build_mpv_kwargs`` — init kwargs only (ytdl=no, load_scripts=no,
  POSIX input_conf null, all the existing playback/audio/network
  tuning). The lavf option is intentionally absent.
- ``lavf_options`` — a dict applied post-construction via the
  python-mpv property API, which uses the node API and accepts
  dict values for keyvalue-list options without splitting on
  commas inside the value.

Tests cover both paths: that ``demuxer_lavf_o`` is NOT in the init
kwargs (regression guard), and that ``lavf_options`` returns the
expected protocol set.

Audit-Ref: SECURITY_AUDIT.md finding #2
Severity: High
2026-04-11 16:34:50 -05:00

89 lines
3.2 KiB
Python

"""Tests for the pure mpv kwargs builder.
Pure Python. No Qt, no mpv, no network. The helper is importable
from the CI environment that installs only httpx + Pillow + pytest.
"""
from __future__ import annotations
from booru_viewer.gui.media._mpv_options import (
LAVF_PROTOCOL_WHITELIST,
build_mpv_kwargs,
lavf_options,
)
def test_ytdl_disabled():
"""Finding #2 — mpv must not delegate URLs to yt-dlp."""
kwargs = build_mpv_kwargs(is_windows=False)
assert kwargs["ytdl"] == "no"
def test_load_scripts_disabled():
"""Finding #2 — no auto-loading of ~/.config/mpv/scripts."""
kwargs = build_mpv_kwargs(is_windows=False)
assert kwargs["load_scripts"] == "no"
def test_protocol_whitelist_not_in_init_kwargs():
"""Finding #2 — the lavf protocol whitelist must NOT be in the
init kwargs dict. python-mpv's init path uses
``mpv_set_option_string``, which trips on the comma-laden value
with -7 OPT_FORMAT. The whitelist is applied separately via the
property API in ``mpv_gl.py`` (see ``lavf_options``)."""
kwargs = build_mpv_kwargs(is_windows=False)
assert "demuxer_lavf_o" not in kwargs
assert "demuxer-lavf-o" not in kwargs
def test_lavf_options_protocol_whitelist():
"""Finding #2 — lavf demuxer must only accept file + HTTP(S) + TLS/TCP.
Returned as a dict so callers can pass it through the python-mpv
property API (which uses the node API and handles comma-laden
values cleanly).
"""
opts = lavf_options()
assert opts.keys() == {"protocol_whitelist"}
allowed = set(opts["protocol_whitelist"].split(","))
# `file` must be present — cached local clips and .part files use it.
assert "file" in allowed
# HTTP(S) + supporting protocols for network videos.
assert "http" in allowed
assert "https" in allowed
assert "tls" in allowed
assert "tcp" in allowed
# Dangerous protocols must NOT appear.
for banned in ("concat", "subfile", "data", "udp", "rtp", "crypto"):
assert banned not in allowed
# The constant and the helper return the same value.
assert opts["protocol_whitelist"] == LAVF_PROTOCOL_WHITELIST
def test_input_conf_nulled_on_posix():
"""Finding #2 — on POSIX, skip loading ~/.config/mpv/input.conf."""
kwargs = build_mpv_kwargs(is_windows=False)
assert kwargs["input_conf"] == "/dev/null"
def test_input_conf_skipped_on_windows():
"""Finding #2 — input_conf gate is POSIX-only; Windows omits the key."""
kwargs = build_mpv_kwargs(is_windows=True)
assert "input_conf" not in kwargs
def test_existing_options_preserved():
"""Regression: pre-audit playback/audio tuning must remain."""
kwargs = build_mpv_kwargs(is_windows=False)
# Discord screen-share audio fix (see mpv_gl.py comment).
assert kwargs["ao"] == "pulse,wasapi,"
assert kwargs["audio_client_name"] == "booru-viewer"
# Network tuning from the uncached-video fast path.
assert kwargs["cache"] == "yes"
assert kwargs["cache_pause"] == "no"
assert kwargs["demuxer_max_bytes"] == "50MiB"
assert kwargs["network_timeout"] == "10"
# Existing input lockdown (primary — input_conf is defense-in-depth).
assert kwargs["input_default_bindings"] is False
assert kwargs["input_vo_keyboard"] is False