fix: disable auto-redirects and validate URLs to prevent SSRF (#3)
- Add _is_safe_url() checking for private/loopback/link-local/reserved IPs - Block internal hostnames (.local, .internal, localhost, metadata.google.internal) - Disable allow_redirects in requests.get() - Validate redirect target before following single hop - Raise RuntimeError on unsafe URL or redirect
This commit is contained in:
parent
8e7f5bc920
commit
fdc66efe1b
@ -42,12 +42,14 @@ Dependencies
|
|||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import datetime
|
import datetime
|
||||||
|
import ipaddress
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Dict, List
|
from typing import Dict, List
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
@ -165,12 +167,42 @@ def fetch_items(start: datetime.datetime, end: datetime.datetime) -> List[Dict]:
|
|||||||
return filtered
|
return filtered
|
||||||
|
|
||||||
|
|
||||||
|
def _is_safe_url(url: str) -> bool:
|
||||||
|
"""Check that a URL points to a safe (non-internal) destination."""
|
||||||
|
parsed = urlparse(url)
|
||||||
|
host = parsed.hostname or ""
|
||||||
|
# Block private and reserved IP ranges
|
||||||
|
try:
|
||||||
|
addr = ipaddress.ip_address(host)
|
||||||
|
if addr.is_private or addr.is_loopback or addr.is_link_local or addr.is_reserved:
|
||||||
|
return False
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
# Block internal hostnames
|
||||||
|
if host.endswith((".local", ".internal", ".home.arpa")) or host in ("localhost", "metadata.google.internal"):
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
def download_file(url: str, dest: Path) -> bool:
|
def download_file(url: str, dest: Path) -> bool:
|
||||||
"""Stream‑download the file at *url* to *dest* if it does not already exist."""
|
"""Stream‑download the file at *url* to *dest* if it does not already exist."""
|
||||||
if dest.exists():
|
if dest.exists():
|
||||||
return False
|
return False
|
||||||
|
if not _is_safe_url(url):
|
||||||
|
raise RuntimeError(f"Unsafe download URL blocked (SSRF protection): {url}")
|
||||||
try:
|
try:
|
||||||
with requests.get(url, stream=True, timeout=30) as r:
|
with requests.get(url, stream=True, timeout=30, allow_redirects=False) as r:
|
||||||
|
if r.status_code in (301, 302, 303, 307, 308):
|
||||||
|
location = r.headers.get("Location", "")
|
||||||
|
if not _is_safe_url(location):
|
||||||
|
raise RuntimeError(f"Redirect to unsafe URL blocked (SSRF protection): {location}")
|
||||||
|
# Follow single validated redirect
|
||||||
|
with requests.get(location, stream=True, timeout=30, allow_redirects=False) as r2:
|
||||||
|
r2.raise_for_status()
|
||||||
|
with dest.open("wb") as f:
|
||||||
|
for chunk in r2.iter_content(chunk_size=8192):
|
||||||
|
f.write(chunk)
|
||||||
|
else:
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
with dest.open("wb") as f:
|
with dest.open("wb") as f:
|
||||||
for chunk in r.iter_content(chunk_size=8192):
|
for chunk in r.iter_content(chunk_size=8192):
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user