A secure Python URL validator does two jobs: it confirms a string is a syntactically valid URL, and it decides whether your application is actually allowed to fetch that URL. Most tutorials only cover the first job with a regular expression, and that is exactly the gap that turns a harmless "fetch this link" feature into a Server-Side Request Forgery (SSRF) hole. If your service ever takes a URL from a user and requests it, validation is a security control, not a formatting nicety.
Let's separate the two concerns, because conflating them is the root of most bad implementations.
Why a regex alone is the wrong tool
The classic approach is a big regex that checks for http:// or https:// and something that looks like a domain. It's brittle and it validates the wrong thing. A regex will happily pass http://169.254.169.254/latest/meta-data/, which on a cloud instance is the metadata endpoint holding your credentials. It also mishandles internationalized domains, userinfo tricks like http://expected.com@evil.com/, and URL-encoded bypasses. Syntactic validity says nothing about whether the URL is safe to visit.
Use the standard library for parsing instead of inventing a pattern. urllib.parse.urlparse gives you structured access to the scheme, host, port, and path:
from urllib.parse import urlparse
def is_well_formed(url: str) -> bool:
try:
parts = urlparse(url)
except ValueError:
return False
return parts.scheme in ("http", "https") and bool(parts.netloc)
That handles the "is this a URL" question far more reliably than a regex. It does not handle "should I fetch this," which is the part that matters.
The validators library for quick checks
If all you need is a boolean "does this look like a valid URL" for a form field, the third-party validators package is a clean choice:
import validators
validators.url("https://example.com") # True
validators.url("not a url") # ValidationFailure (falsy)
It's lightweight and well-suited to input hygiene on data you will store or display, not fetch. Keep in mind it's still only checking form. A string passing validators.url can still point straight at your internal network. Treat it as a first gate, never the last one.
Pydantic for typed, structured validation
In an application already using Pydantic (FastAPI backends, config models), lean on its URL types rather than validating by hand. Pydantic v2 ships HttpUrl and AnyUrl, which parse and normalize the value during model validation:
from pydantic import BaseModel, HttpUrl
class WebhookConfig(BaseModel):
callback: HttpUrl
WebhookConfig(callback="https://example.com/hook") # ok
WebhookConfig(callback="ftp://example.com") # raises ValidationError
This gives you validation at the boundary of your API for free, with good error messages. It enforces scheme and structure, so it's a strong default for request bodies. Again, though, it is a syntax and scheme check, not an SSRF defense.
The part that actually prevents SSRF
SSRF is the reason a URL validator belongs in a security review at all. The fix is an allowlist mindset plus resolving the host before you connect. Here is the shape of a validator that decides whether a URL is safe to request server-side:
import ipaddress
import socket
from urllib.parse import urlparse
BLOCKED_SCHEMES = {"file", "gopher", "ftp"}
def is_safe_to_fetch(url: str, allowed_hosts: set[str]) -> bool:
parts = urlparse(url)
if parts.scheme not in ("http", "https"):
return False
host = parts.hostname
if not host or host not in allowed_hosts:
return False
# Resolve and reject private / loopback / link-local ranges
try:
for info in socket.getaddrinfo(host, None):
ip = ipaddress.ip_address(info[4][0])
if ip.is_private or ip.is_loopback or ip.is_link_local or ip.is_reserved:
return False
except socket.gaierror:
return False
return True
Three ideas do the heavy lifting. First, an explicit allowlist of hosts you intend to talk to beats any blocklist, because blocklists are always incomplete. Second, resolve the hostname and inspect the resulting IP against private, loopback, and link-local ranges, which blocks the cloud metadata endpoint and internal services. Third, restrict schemes so nobody sneaks in file:// or gopher://.
Two traps remain even with this code. DNS rebinding can return a public IP at validation time and a private one at fetch time, so the safest pattern is to resolve once and connect to that exact resolved IP, pinning it. And HTTP redirects can bounce a whitelisted host to an internal target, so disable automatic redirects in your HTTP client or re-run the check on every redirect target. This attack class is category API7:2023 in the OWASP API Top Ten, and it shows up constantly in webhook and "import from URL" features.
Putting it together in a request
Validate first, then fetch with redirects disabled and the resolved IP pinned:
import requests
def fetch(url: str, allowed_hosts: set[str]) -> requests.Response:
if not is_safe_to_fetch(url, allowed_hosts):
raise ValueError("URL failed security validation")
return requests.get(url, allow_redirects=False, timeout=5)
The timeout matters too. An unbounded request against an attacker-controlled slow endpoint is a resource-exhaustion vector on its own.
Where dependency risk sneaks in
A URL validator often pulls in helper libraries, and those libraries carry their own history. Older versions of some Python packages that parse URLs or IP addresses have had bypass CVEs where a crafted input slipped past the check. That's a good reason to keep validation dependencies current and scanned. An SCA tool such as Safeguard can flag when a URL-parsing or HTTP library in your tree has a known bypass, including transitive ones you didn't add directly. Pin versions, review changelogs for security fixes, and don't assume a validation library is safe just because it's popular.
The short version: use the standard library or Pydantic to confirm structure, use an allowlist plus IP-range checks to confirm safety, disable redirects, set timeouts, and keep the libraries patched. Do that, and your Python URL validator is a control instead of a false sense of security.
FAQ
What is the best Python URL validator library?
For form-field hygiene, the validators package is simple and effective. For typed API inputs, Pydantic's HttpUrl is the cleanest option. Neither prevents SSRF on its own; you still need allowlist and IP-range checks before fetching a URL server-side.
Does urlparse validate a URL?
urllib.parse.urlparse parses a URL into components but does not reject invalid ones. Check the parsed scheme and netloc yourself, or combine it with a dedicated validator. It's a parser, not a validator.
How do I prevent SSRF in a Python URL validator?
Use an explicit allowlist of permitted hosts, resolve the hostname to an IP and reject private, loopback, and link-local ranges, restrict schemes to http and https, disable HTTP redirects, and pin the connection to the resolved IP to defeat DNS rebinding.
Is a regex enough to validate URLs in Python?
No. A regex checks form, not safety, and mishandles encoded inputs, userinfo tricks, and internal addresses. Prefer urlparse or Pydantic for structure and a dedicated safety check for whether the URL should be fetched.