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

View file

@ -1,10 +1,13 @@
import copy import copy
from datetime import datetime, timezone from datetime import datetime, timezone
from functools import lru_cache
import ipaddress
import json import json
import logging import logging
import os import os
import pathlib import pathlib
import re import re
import socket
from typing import Any from typing import Any
import uuid import uuid
import yt_dlp 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 get_value(default)
return current 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