From 1185fa7bdf0a93da1c4d80495b8e699665cea26c Mon Sep 17 00:00:00 2001 From: wyytjh Date: Mon, 29 Jun 2026 22:20:28 +0800 Subject: [PATCH] Fix path traversal vulnerability in resolve_path() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Security fix for CWE-22: The resolve_path() function in helpers/render.py did not validate file paths, allowing an attacker to use '../' sequences in EDL JSON files to read arbitrary files from the filesystem via ffmpeg. Three attack vectors are fixed: 1. Source paths (sources[] field) — passed to ffmpeg -i 2. Subtitle paths (subtitles field) — passed to ffmpeg subtitles filter 3. Overlay paths (overlays[].file field) — passed to ffmpeg -i The fix: - Rejects absolute paths - Uses Path.resolve() (symlink-safe) before containment check - Validates resolved path is within the intended base directory - Compatible with Python 3.9+ (is_relative_to) and older versions Co-Authored-By: Claude --- helpers/render.py | 37 ++++++++++++++++++++++++++++++++++--- 1 file changed, 34 insertions(+), 3 deletions(-) diff --git a/helpers/render.py b/helpers/render.py index 0d02cffa..59f5a18a 100644 --- a/helpers/render.py +++ b/helpers/render.py @@ -23,6 +23,7 @@ import argparse import json +import os import re import subprocess import sys @@ -85,11 +86,41 @@ def resolve_grade_filter(grade_field: str | None) -> str: def resolve_path(maybe_path: str, base: Path) -> Path: - """Resolve a path that may be absolute or relative to `base`.""" + """Resolve a path that may be absolute or relative to `base`. + + Security: This function validates that the resolved path stays within + the intended ``base`` directory to prevent path traversal attacks. + Absolute paths are rejected. Symlink-based bypasses are mitigated by + calling ``Path.resolve()`` (which resolves symlinks) **before** the + containment check. + + Raises: + ValueError: If ``maybe_path`` is absolute, or if the resolved path + falls outside ``base``. + """ p = Path(maybe_path) if p.is_absolute(): - return p - return (base / p).resolve() + raise ValueError(f"Absolute paths are not allowed: {maybe_path}") + + resolved = (base / p).resolve() + base_resolved = base.resolve() + + # Python 3.9+: use Path.is_relative_to() + if hasattr(resolved, "is_relative_to"): + if not resolved.is_relative_to(base_resolved): + raise ValueError( + f"Path traversal detected: {maybe_path} resolves to {resolved}, " + f"which is outside the allowed directory {base_resolved}" + ) + else: + # Fallback for Python < 3.9: string prefix check + if not str(resolved).startswith(str(base_resolved)): + raise ValueError( + f"Path traversal detected: {maybe_path} resolves to {resolved}, " + f"which is outside the allowed directory {base_resolved}" + ) + + return resolved # -------- HDR → SDR tone mapping (HLG / PQ sources) --------------------------