added LFI protection for thumbnails

This commit is contained in:
ArabCoders 2024-12-17 17:25:16 +03:00
parent c727ab3446
commit 45227be6bf
2 changed files with 60 additions and 8 deletions

View file

@ -4,7 +4,6 @@ import functools
import json
import os
import time
import httpx
from .config import Config
from .DownloadQueue import DownloadQueue
@ -20,6 +19,7 @@ from .common import common
from pathlib import Path
from .encoder import Encoder
from .Emitter import Emitter
from .Utils import validate_url
LOG = logging.getLogger('app')
MIME = magic.Magic(mime=True)
@ -527,6 +527,11 @@ class HttpAPI(common):
if not url:
return web.json_response({"error": "URL is required."}, status=400)
try:
validate_url(url)
except Exception as e:
return web.json_response({"error": str(e)}, status=400)
try:
opts = {
'proxy': self.config.ytdl_options.get('proxy', None),
@ -539,13 +544,13 @@ class HttpAPI(common):
async with httpx.AsyncClient(**opts) as client:
LOG.info(f"Fetching thumbnail from '{url}'.")
response = await client.request(method='GET', url=url)
return web.Response(body=response.content, headers={
'Content-Type': response.headers.get('Content-Type'),
'Pragma': 'public',
'Access-Control-Allow-Origin': '*',
'Cache-Control': f"public, max-age={time.time() + 31536000}",
'Expires': time.strftime('%a, %d %b %Y %H:%M:%S GMT', datetime.fromtimestamp(time.time() + 31536000).timetuple()),
})
return web.Response(body=response.content,
headers={'Content-Type': response.headers.get('Content-Type'),
'Pragma': 'public', 'Access-Control-Allow-Origin': '*',
'Cache-Control': f"public, max-age={time.time() + 31536000}",
'Expires': time.strftime(
'%a, %d %b %Y %H:%M:%S GMT', datetime.fromtimestamp(
time.time() + 31536000).timetuple()), })
except Exception as e:
LOG.error(f"Error fetching thumbnail from '{url}'. '{e}'")
return web.json_response({"error": str(e)}, status=500)

View file

@ -1,10 +1,13 @@
import copy
from datetime import datetime, timezone
from functools import lru_cache
import ipaddress
import json
import logging
import os
import pathlib
import re
import socket
from typing import Any
import uuid
import yt_dlp
@ -359,3 +362,47 @@ def ag(array: dict | list, path: list[str | int] | str | int, default: Any = Non
return get_value(default)
return current
@lru_cache(maxsize=512)
def is_private_address(hostname: str) -> bool:
try:
ip = socket.gethostbyname(hostname)
ip_obj = ipaddress.ip_address(ip)
return (ip_obj.is_private or ip_obj.is_loopback or ip_obj.is_reserved or ip_obj.is_link_local)
except socket.gaierror:
# Could not resolve - treat as invalid or restricted
return True
def validate_url(url: str) -> bool:
"""
Validate if the url is valid and allowed.
Args:
url (str): URL to validate.
Raises:
ValueError: If the URL is invalid or not allowed.
Returns:
bool: True if the URL is valid and allowed.
"""
if not url:
raise ValueError("URL is required.")
try:
from yarl import URL
parsed_url = URL(url)
except ValueError:
raise ValueError("Invalid URL.")
# Check allowed schemes
if parsed_url.scheme not in ["http", "https"]:
raise ValueError("Invalid scheme usage. Only HTTP or HTTPS allowed.")
hostname = parsed_url.host
if not hostname or is_private_address(hostname):
raise ValueError("Access to internal urls or private networks is not allowed.")
return True