security: harden code against command injection and path traversal
Core changes: * enforce HTTPS URLs and remove shell usage in generate_release.py * replace os.system calls with subprocess across the codebase * validate external inputs (playlist names, video IDs) Improvements and fixes: * settings.py: fix typo (node.lineno → line_number); use isinstance() over type() * youtube/get_app_version: improve git detection using subprocess.DEVNULL * youtube/util.py: add cleanup helpers; use shutil.which for binary resolution YouTube modules: * watch.py: detect and flag HLS streams; remove unused audio_track_sources * comments.py: return early when comments are disabled; add error handling * local_playlist.py: validate playlist names to prevent path traversal * subscriptions.py: replace asserts with proper error handling; validate video IDs Cleanup: * remove unused imports across modules (playlist, search, channel) * reorganize package imports in youtube/**init**.py * simplify test imports and fix cleanup_func in tests Tests: * tests/test_shorts.py: simplify imports * tests/test_util.py: fix cleanup_func definition
This commit is contained in:
@@ -1,28 +1,42 @@
|
||||
from youtube import util, yt_data_extract
|
||||
from youtube import util
|
||||
from youtube import yt_app
|
||||
import settings
|
||||
|
||||
import os
|
||||
import json
|
||||
import html
|
||||
import gevent
|
||||
import urllib
|
||||
import math
|
||||
import glob
|
||||
import re
|
||||
|
||||
import flask
|
||||
from flask import request
|
||||
|
||||
playlists_directory = os.path.join(settings.data_dir, "playlists")
|
||||
thumbnails_directory = os.path.join(settings.data_dir, "playlist_thumbnails")
|
||||
playlists_directory = os.path.join(settings.data_dir, 'playlists')
|
||||
thumbnails_directory = os.path.join(settings.data_dir, 'playlist_thumbnails')
|
||||
|
||||
# Whitelist accepted playlist names so user input cannot escape
|
||||
# `playlists_directory` / `thumbnails_directory` (CWE-22, OWASP A01:2021).
|
||||
# Allow letters, digits, spaces, dot, dash and underscore.
|
||||
_PLAYLIST_NAME_RE = re.compile(r'^[\w .\-]{1,128}$')
|
||||
|
||||
|
||||
def _validate_playlist_name(name):
|
||||
'''Return the stripped name if safe, otherwise abort with 400.'''
|
||||
if name is None:
|
||||
flask.abort(400)
|
||||
name = name.strip()
|
||||
if not _PLAYLIST_NAME_RE.match(name):
|
||||
flask.abort(400)
|
||||
return name
|
||||
|
||||
|
||||
def _find_playlist_path(name):
|
||||
"""Find playlist file robustly, handling trailing spaces in filenames"""
|
||||
name = name.strip()
|
||||
pattern = os.path.join(playlists_directory, name + "*.txt")
|
||||
'''Find playlist file robustly, handling trailing spaces in filenames'''
|
||||
name = _validate_playlist_name(name)
|
||||
pattern = os.path.join(playlists_directory, name + '*.txt')
|
||||
files = glob.glob(pattern)
|
||||
return files[0] if files else os.path.join(playlists_directory, name + ".txt")
|
||||
return files[0] if files else os.path.join(playlists_directory, name + '.txt')
|
||||
|
||||
|
||||
def _parse_playlist_lines(data):
|
||||
@@ -179,8 +193,9 @@ def path_edit_playlist(playlist_name):
|
||||
redirect_page_number = min(int(request.values.get('page', 1)), math.ceil(number_of_videos_remaining/50))
|
||||
return flask.redirect(util.URL_ORIGIN + request.path + '?page=' + str(redirect_page_number))
|
||||
elif request.values['action'] == 'remove_playlist':
|
||||
safe_name = _validate_playlist_name(playlist_name)
|
||||
try:
|
||||
os.remove(os.path.join(playlists_directory, playlist_name + ".txt"))
|
||||
os.remove(os.path.join(playlists_directory, safe_name + '.txt'))
|
||||
except OSError:
|
||||
pass
|
||||
return flask.redirect(util.URL_ORIGIN + '/playlists')
|
||||
@@ -220,8 +235,17 @@ def edit_playlist():
|
||||
flask.abort(400)
|
||||
|
||||
|
||||
_THUMBNAIL_RE = re.compile(r'^[A-Za-z0-9_-]{11}\.jpg$')
|
||||
|
||||
|
||||
@yt_app.route('/data/playlist_thumbnails/<playlist_name>/<thumbnail>')
|
||||
def serve_thumbnail(playlist_name, thumbnail):
|
||||
# .. is necessary because flask always uses the application directory at ./youtube, not the working directory
|
||||
# Validate both path components so a crafted URL cannot escape
|
||||
# `thumbnails_directory` via `..` or NUL tricks (CWE-22).
|
||||
safe_name = _validate_playlist_name(playlist_name)
|
||||
if not _THUMBNAIL_RE.match(thumbnail):
|
||||
flask.abort(400)
|
||||
# .. is necessary because flask always uses the application directory at
|
||||
# ./youtube, not the working directory.
|
||||
return flask.send_from_directory(
|
||||
os.path.join('..', thumbnails_directory, playlist_name), thumbnail)
|
||||
os.path.join('..', thumbnails_directory, safe_name), thumbnail)
|
||||
|
||||
Reference in New Issue
Block a user