-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsafe_fetch.py
More file actions
71 lines (60 loc) · 2.45 KB
/
safe_fetch.py
File metadata and controls
71 lines (60 loc) · 2.45 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
# Copyright (c) 2026 Simon HGR — instockornot.club — ELv2 License
"""
safe_fetch.py — SSRF-safe URL fetching for Drop Watcher.
Blocks requests to private/internal IPs, metadata endpoints, and non-HTTP schemes.
Used by watcher_signup.py (check-url) and per_user_alerter.py.
HGR
"""
import ipaddress
import socket
from urllib.parse import urlparse
# Private/reserved IP ranges that should never be fetched
BLOCKED_NETWORKS = [
ipaddress.ip_network('127.0.0.0/8'), # loopback
ipaddress.ip_network('10.0.0.0/8'), # private
ipaddress.ip_network('172.16.0.0/12'), # private
ipaddress.ip_network('192.168.0.0/16'), # private
ipaddress.ip_network('169.254.0.0/16'), # link-local / cloud metadata
ipaddress.ip_network('0.0.0.0/8'), # unspecified
ipaddress.ip_network('100.64.0.0/10'), # carrier-grade NAT
ipaddress.ip_network('198.18.0.0/15'), # benchmarking
ipaddress.ip_network('::1/128'), # IPv6 loopback
ipaddress.ip_network('fc00::/7'), # IPv6 private
ipaddress.ip_network('fe80::/10'), # IPv6 link-local
]
def is_safe_url(url):
"""
Validate that a URL is safe to fetch:
- Must be http:// or https://
- Hostname must resolve to a public IP (not private/internal)
Returns (safe: bool, reason: str)
"""
try:
parsed = urlparse(url)
except Exception:
return False, "Invalid URL."
# Scheme check
if parsed.scheme not in ('http', 'https'):
return False, "Only http:// and https:// URLs are supported."
# Must have a hostname
hostname = parsed.hostname
if not hostname:
return False, "No hostname found in URL."
# Block obvious metadata hostnames
if hostname in ('metadata.google.internal', 'metadata', 'instance-data'):
return False, "That URL is not allowed."
# Resolve hostname to IP and check against blocklist
try:
addrinfo = socket.getaddrinfo(hostname, None, socket.AF_UNSPEC, socket.SOCK_STREAM)
except socket.gaierror:
return False, "We can't resolve that hostname. Check the URL."
for family, _, _, _, sockaddr in addrinfo:
ip_str = sockaddr[0]
try:
ip = ipaddress.ip_address(ip_str)
except ValueError:
continue
for network in BLOCKED_NETWORKS:
if ip in network:
return False, "That URL points to an internal or reserved address."
return True, "ok"