mirror of
https://github.com/vatax3/ranger.git
synced 2026-07-29 23:49:11 +00:00
Some checks are pending
Build & Push Docker image / build (push) Waiting to run
Suite à un audit externe du dépôt, correction des vulnérabilités confirmées
après vérification manuelle du code :
P0 (critiques) :
- RealDebrid : suppression d'un serveur StremThru tiers codé en dur
(stremthru.13377001.xyz) auquel la clé API RD était envoyée à l'insu de
l'utilisateur dès lors qu'aucune StremThru propre n'était configurée.
Remplacé par un vrai client API Real-Debrid officiel (api.real-debrid.com
exclusivement).
- XSS réfléchie sur /configure : la config décodée était injectée brute
(json.dumps) dans un <script> inline, une valeur contenant "</script>"
cassait hors du tag. Échappement des caractères dangereux en séquences
unicode avant injection.
- XSS stockée dans le dashboard admin : un ID Stremio non validé entrait
dans les clés de cache puis était rendu via innerHTML sans échappement
(dont un point d'injection via onclick jamais échappé du tout). Validation
stricte de l'IMDB ID côté serveur (^tt\d{5,9}$) + reconstruction du
dashboard sans innerHTML/onclick interpolés (DOM API + addEventListener).
- Fuite de cache inter-comptes : la clé de cache recherche et la clé de
cache des liens résolus ne dépendaient pas des identifiants utilisés,
seulement de l'IMDB/hash. Deux profils différents (ex: plusieurs membres
d'une famille) partageant la même instance pouvaient recevoir les
résultats ou liens streaming de l'autre. Namespacing par empreinte des
credentials (core.cache.fingerprint) pour les trackers/débrideurs à clé ;
cache resté partagé pour les trackers publics (pas de credential, pas de
fuite).
- SSRF via Torznab/UNIT3D : les URLs d'indexeurs viennent de la config
utilisateur, or Ranger est exposé publiquement (config auto-suffisante,
pas besoin de connaître de clé). Ajout de core/netsafety.py qui résout et
bloque les cibles loopback/link-local/réservées (dont le 169.254.169.254
des métadonnées cloud) avant chaque requête ; RFC1918 reste autorisé
(usage prévu : Jackett/Prowlarr auto-hébergé sur le LAN de l'opérateur).
Corrections additionnelles :
- Token admin : comparaison à temps constant (hmac.compare_digest) et
suppression du fallback ?token= en query string (loggable par les CDN/proxy).
- Conteneur Docker : utilisateur non-root (UID/GID 1000 fixes).
- RANGER_PUBLIC_URL (optionnel) : URL publique fixe prioritaire sur les
en-têtes X-Forwarded-Proto/Host, potentiellement usurpables si le port du
conteneur est aussi joignable directement.
- Détection anime : le genre Animation (16) est désormais requis dans tous
les cas — un "or orig_lang == 'ja'" classait n'importe quel drama
live-action japonais comme anime.
- Numérotation absolue transmise jusqu'à /resolve (elle ne servait qu'au
filtrage) : la sélection du bon fichier dans un pack anime fansub peut
désormais matcher l'épisode absolu en plus de SxxExx.
- Budget de 20s sur la vérification de disponibilité multi-débrideurs : un
backend lent/hors service ne bloque plus toute la réponse /stream.
- UNIT3D : suppression de download_torrent (code mort, jamais appelé) et
réduction du logging de réponse brute (risque de fuite de tokens signés).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
65 lines
2.4 KiB
Python
65 lines
2.4 KiB
Python
"""
|
|
Protection SSRF pour les URLs fournies par la configuration utilisateur
|
|
(indexeurs Torznab, trackers UNIT3D).
|
|
|
|
Ranger est exposé publiquement : n'importe qui peut se construire sa propre
|
|
config (le schéma base64/JSON est public, pas besoin de connaître les clés
|
|
de qui que ce soit) et faire pointer un indexeur vers une adresse interne —
|
|
LAN du VPS/homelab, localhost, endpoint de métadonnées cloud
|
|
(169.254.169.254), API Docker locale, etc. Ce module valide l'IP réellement
|
|
résolue avant chaque requête sortante vers ces URLs utilisateur.
|
|
"""
|
|
|
|
import asyncio
|
|
import ipaddress
|
|
import logging
|
|
import socket
|
|
from urllib.parse import urlparse
|
|
|
|
|
|
def _is_blocked_ip(ip_str):
|
|
try:
|
|
ip = ipaddress.ip_address(ip_str)
|
|
except ValueError:
|
|
return True # IP illisible : on bloque par prudence
|
|
|
|
# On bloque loopback (peut taper l'API admin ou un service local du VPS),
|
|
# link-local — ce qui couvre 169.254.169.254, l'endpoint de métadonnées
|
|
# cloud utilisé par AWS/GCP/Azure/OCI — et les plages réservées/multicast.
|
|
#
|
|
# Les plages privées RFC1918 (10.x, 172.16-31.x, 192.168.x) restent
|
|
# volontairement AUTORISÉES : c'est la cible normale d'un Jackett/Prowlarr
|
|
# auto-hébergé sur le LAN de l'opérateur, l'usage prévu de Torznab. Un
|
|
# blocage total casserait cette fonctionnalité pour son propre usage.
|
|
return (
|
|
ip.is_loopback or ip.is_link_local
|
|
or ip.is_multicast or ip.is_reserved or ip.is_unspecified
|
|
)
|
|
|
|
|
|
async def is_url_safe(url):
|
|
"""
|
|
Résout l'hôte de l'URL et vérifie qu'aucune IP résolue n'est privée,
|
|
loopback, link-local (couvre le 169.254.169.254 des métadonnées cloud)
|
|
ou réservée. Rejette aussi les schémas autres que http/https.
|
|
"""
|
|
try:
|
|
parsed = urlparse(url)
|
|
if parsed.scheme not in ("http", "https"):
|
|
return False
|
|
host = parsed.hostname
|
|
if not host:
|
|
return False
|
|
|
|
loop = asyncio.get_event_loop()
|
|
infos = await loop.run_in_executor(None, socket.getaddrinfo, host, None)
|
|
ips = {info[4][0] for info in infos}
|
|
if not ips:
|
|
return False
|
|
if any(_is_blocked_ip(ip) for ip in ips):
|
|
logging.warning(f"SSRF bloqué : {host} résout vers une IP interne ({ips})")
|
|
return False
|
|
return True
|
|
except Exception as e:
|
|
logging.warning(f"Vérification SSRF échouée pour {url!r}: {e}")
|
|
return False
|