Samad14's picture
fix(blast): poll cap 65min, DNA validation, program/db/max_hits params
cd0c7a9 verified
Raw
History Blame Contribute Delete
2.9 kB
"""SSRF protection utilities.
Validates that user-supplied URLs point to allowed public hosts and
blocks requests to private/reserved IP ranges.
"""
from __future__ import annotations
import ipaddress
import logging
from urllib.parse import urlparse
from fastapi import HTTPException
logger = logging.getLogger(__name__)
ALLOWED_HOSTS: set[str] = {
"files.rcsb.org",
"data.rcsb.org",
"search.rcsb.org",
"www.rcsb.org",
"alphafold.ebi.ac.uk",
"rest.uniprot.org",
"www.uniprot.org",
}
PRIVATE_NETWORKS = [
ipaddress.ip_network("127.0.0.0/8"),
ipaddress.ip_network("10.0.0.0/8"),
ipaddress.ip_network("172.16.0.0/12"),
ipaddress.ip_network("192.168.0.0/16"),
ipaddress.ip_network("169.254.0.0/16"),
ipaddress.ip_network("::1/128"),
ipaddress.ip_network("fc00::/7"),
ipaddress.ip_network("fe80::/10"),
]
def validate_url(url: str, param_name: str = "url") -> None:
"""Validate a user-supplied URL against SSRF protections.
Checks:
1. URL is well-formed
2. Host is in the allowlist
3. Resolved IP is not in a private/reserved range
Raises HTTPException(400) on violation.
"""
if not url or not url.strip():
return
parsed = urlparse(url)
if parsed.scheme not in ("http", "https"):
raise HTTPException(status_code=400, detail=f"{param_name}: only http/https URLs are allowed")
hostname = parsed.hostname
if not hostname:
raise HTTPException(status_code=400, detail=f"{param_name}: invalid URL — no hostname")
# Check allowlist (suffix match to allow subdomains)
host_allowed = any(
hostname == allowed or hostname.endswith("." + allowed)
for allowed in ALLOWED_HOSTS
)
if not host_allowed:
raise HTTPException(
status_code=400,
detail=f"{param_name}: host '{hostname}' is not in the allowed list. "
f"Allowed: {', '.join(sorted(ALLOWED_HOSTS))}",
)
# Resolve IP and check for private ranges
try:
import socket
addrinfos = socket.getaddrinfo(hostname, None, socket.AF_UNSPEC, socket.SOCK_STREAM)
for family, _, _, _, sockaddr in addrinfos:
ip = ipaddress.ip_address(sockaddr[0])
for net in PRIVATE_NETWORKS:
if ip in net:
raise HTTPException(
status_code=400,
detail=f"{param_name}: resolved to private IP {ip} — request blocked",
)
except HTTPException:
raise
except Exception as e:
logger.warning(f"SSRF DNS check failed for {hostname}: {e}")
# If DNS resolution fails, block the request rather than allowing it through
raise HTTPException(
status_code=400,
detail=f"{param_name}: could not resolve hostname — request blocked",
)