SPB Git

spb/trouve-ka Public

Trouve-KA — moteur de recherche web indépendant, Québec-first. Crawler distribué, index OpenSearch, ranking bilingue, galerie d'images. En prod : www.trouve-ka.com

Python 76.8% TypeScript 15.7% SQL 3.9% Shell 1.4% CSS 1.3% Dockerfile 0.7%
2.0 KB · 76 lines python
Raw Blame History
1# Trouve-KA — prévention SSRF2# Author: Simon-Pierre Boucher3# Contact: contact@spboucher.ai45"""Garde SSRF du crawler (CLAUDE.md §5.7).67Bloque localhost, plages privées IPv4/IPv6, link-local, métadonnées cloud,8schémas non-HTTP. La résolution DNS est revalidée à chaque saut de redirection9par l'appelant (fetcher).10"""1112import ipaddress13import socket14from urllib.parse import urlsplit1516BLOCKED_HOSTS = {17    "localhost",18    "metadata.google.internal",19    "metadata.gke.internal",20}2122# Endpoint de métadonnées AWS/GCP/Azure/OpenStack23METADATA_IPS = {"169.254.169.254", "fd00:ec2::254"}242526def is_safe_ip(ip_str: str) -> bool:27    try:28        ip = ipaddress.ip_address(ip_str)29    except ValueError:30        return False31    if ip_str in METADATA_IPS:32        return False33    return not (34        ip.is_private35        or ip.is_loopback36        or ip.is_link_local37        or ip.is_multicast38        or ip.is_reserved39        or ip.is_unspecified40    )414243def resolve_host(host: str) -> list[str]:44    """Résout un hôte en IPs (IPv4+IPv6). Lève socket.gaierror si introuvable."""45    infos = socket.getaddrinfo(host, None, proto=socket.IPPROTO_TCP)46    return list({info[4][0] for info in infos})474849def is_safe_url(url: str, *, resolve: bool = True) -> bool:50    """Vérifie qu'une URL est sûre à fetcher (schéma, hôte, IPs résolues)."""51    try:52        parts = urlsplit(url)53    except ValueError:54        return False55    if parts.scheme.lower() not in ("http", "https"):56        return False57    host = parts.hostname58    if not host:59        return False60    host = host.lower().strip(".")61    if host in BLOCKED_HOSTS or host.endswith(".localhost") or host.endswith(".internal"):62        return False63    # Hôte littéral IP64    try:65        ipaddress.ip_address(host)66        return is_safe_ip(host)67    except ValueError:68        pass69    if not resolve:70        return True71    try:72        ips = resolve_host(host)73    except (socket.gaierror, OSError):74        return False75    return bool(ips) and all(is_safe_ip(ip) for ip in ips)76