Your URL Security Guardian - Protecting Against SSRF, XSS, and 16+ Attack Vectors
Project description
urlpolice
๐จ Stop Bad URLs Before They Stop You.
The security-first Python library that stands guard between your application and the wild west of user-submitted URLs.
One import. One call. Sixteen battle-tested checks. Zero excuses for SSRF.
Installation โข Quick Start โข Features โข Why urlpolice? โข Configuration โข Presets โข Contributing
๐ก The Problem
Every backend that accepts a URL is a loaded gun pointed at your infrastructure. AWS credentials stolen via http://169.254.169.254. Internal services exposed through http://localhost:6379. Data exfiltrated through DNS rebinding. Firewalls bypassed with octal-encoded IPs.
Writing these checks yourself means tracking dozens of RFCs, encoding tricks, and an ever-growing list of CVEs. Miss one, and you're the next breach headline.
๐ The Solution
from urlpolice import URLPolice
police = URLPolice()
# โ
Safe โ passes all 16 checks
result = police.validate("https://api.example.com/v2/users")
assert result.is_valid
# ๐ซ Blocked โ SSRF via cloud metadata
result = police.validate("http://169.254.169.254/latest/meta-data/")
assert not result.is_valid
# ๐ซ Blocked โ encoded localhost bypass
result = police.validate("http://0x7f000001/admin")
assert not result.is_valid
# ๐ซ Blocked โ path traversal
result = police.validate("https://cdn.example.com/../../etc/passwd")
assert not result.is_valid
One import. One call. Sleep at night.
๐ฆ Installation
Using uv (recommended)
# Create a virtual environment and activate it
uv venv
source .venv/bin/activate # macOS / Linux
# .venv\Scripts\activate # Windows
# Install urlpolice
uv add urlpolice
# With optional DNS resolution support
uv add "urlpolice[dns]"
Using pip
# Create a virtual environment and activate it
python -m venv .venv
source .venv/bin/activate # macOS / Linux
# .venv\Scripts\activate # Windows
# Install urlpolice
pip install urlpolice
# With optional DNS resolution support
pip install "urlpolice[dns]"
Development Setup
# Clone the repo and install in editable mode with dev tools
git clone https://github.com/urlpolice/urlpolice.git
cd urlpolice
# With uv
uv venv && source .venv/bin/activate
uv sync
# Or with pip
python -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"
Requires: Python 3.10+
๐ Quick Start
from urlpolice import URLPolice
police = URLPolice()
result = police.validate("https://example.com/page")
if result.is_valid:
print("โ
Safe URL:", result.url)
else:
for error in result.errors:
print("๐ซ Blocked:", error)
for warning in result.warnings:
print("โ ๏ธ Warning:", warning)
The ValidationResult gives you structured, actionable output:
| Attribute | Type | Description |
|---|---|---|
is_valid |
bool |
Whether the URL passed all checks |
url |
str | None |
Normalized URL (only set when valid) |
errors |
tuple[str, ...] |
All error messages |
warnings |
tuple[str, ...] |
Non-blocking warnings |
metadata |
dict |
Contextual info (e.g. {"original_url": "..."}) |
๐ก Pro tip:
ValidationResultis truthy/falsy โ useif result:directly in conditionals.
๐ก๏ธ Features
urlpolice runs 12 specialized security modules covering 16+ attack vectors in a carefully ordered pipeline:
| Category | What It Catches | |
|---|---|---|
| ๐ | SSRF Protection | Private IPs, loopback, link-local, cloud metadata (AWS, GCP, Azure, Alibaba, Oracle), encoded IP bypasses (hex, octal, decimal, IPv4-mapped IPv6) |
| ๐ | XSS Detection | javascript:, data:, vbscript: URI schemes, <script> injection in fragments and paths |
| ๐ | Path Traversal | ../ sequences, percent-encoded variants (%2e%2e%2f), double-encoded (%252e), overlong UTF-8 (%c0%af) |
| ๐ | DNS Rebinding | Resolves hostnames at validation time, checks every resolved IP against private ranges |
| โฉ๏ธ | Open Redirect | Scans 19 common redirect parameter names (url=, next=, goto=, callback=, etc.) |
| ๐ญ | Homograph Attacks | Detects Cyrillic/Greek look-alike characters disguised as Latin (ะฐpple.com โ apple.com) |
| ๐ | Credential Leakage | Rejects embedded user:pass@ in URLs, detects UNC path (\\server\share) attempts |
| ๐ | Injection | Null-byte (%00) and CRLF (%0d%0a) injection โ hard fail, early exit |
| ๐ค | Encoding Abuse | Double-encoding, triple-encoding, overlong UTF-8 percent sequences |
| ๐ | Scheme Allowlist | Configurable; blocks 22 dangerous schemes (file://, gopher://, dict://, etc.) |
| ๐ช | Port Control | Optional port allowlist, warnings for dangerous ports (Redis 6379, MySQL 3306, SSH 22, ...) |
| ๐ | Domain Lists | Per-instance allow/blocklists, DNS label length enforcement (RFC 1035), URL length cap |
โจ And More
| Capability | Description | |
|---|---|---|
| ๐ฆ | Batch Validation | validate_batch() for processing URL lists in a single call |
| โ๏ธ | 4 Built-in Presets | strict, permissive, webhook, user_content |
| ๐ | File-Based Config | Load settings from TOML or JSON โ no code changes needed |
| ๐งฉ | Modular Checks | Import and run any check module individually |
| ๐๏ธ | Selective Disabling | Skip checks you don't need via disabled_checks |
| ๐งต | Thread-Safe DNS Cache | TTL-based caching for high-throughput validation |
| ๐ | Minimal Dependencies | Only idna (pure Python); DNS uses stdlib socket |
| ๐ง | Immutable Config | Frozen dataclasses โ safe to share across threads |
๐ Why urlpolice Over Alternatives?
There are other URL validation tools in the Python ecosystem. Here's why urlpolice exists and where it fits:
| Feature | urlpolice | validators | Pydantic HttpUrl |
ssrf-protect | SafeURL / Advocate | |
|---|---|---|---|---|---|---|
| ๐ | SSRF (private IP blocking) | โ | โ ๏ธ public=True |
โ | โ | โ |
| โ๏ธ | Cloud metadata blocking | โ 5 providers | โ | โ | โ | โ |
| ๐งฎ | Encoded IP detection (hex/octal/decimal) | โ | โ | โ | โ | โ |
| ๐ | DNS rebinding protection | โ | โ | โ | โ | โ ๏ธ Partial |
| ๐ญ | Homograph/IDN attack detection | โ | โ | โ | โ | โ |
| ๐ | Path traversal detection | โ | โ | โ | โ | โ |
| ๐ | XSS scheme detection | โ | โ | โ | โ | โ |
| ๐ | CRLF / null-byte injection | โ | โ | โ | โ | โ |
| ๐ค | Double/overlong encoding bypass | โ | โ | โ | โ | โ |
| โฉ๏ธ | Open redirect param scanning | โ | โ | โ | โ | โ |
| ๐ | Credential leakage detection | โ | โ | โ | โ | โ |
| ๐ช | Port allowlist + dangerous port warnings | โ | โ | โ | โ | โ |
| ๐ | TOML / JSON config files | โ | โ | โ | โ | โ |
| โ๏ธ | Ready-made presets | โ 4 presets | โ | โ | โ | โ |
| ๐งฉ | Modular (use checks individually) | โ | โ | โ | โ | โ |
| ๐งต | Thread-safe DNS cache | โ | โ | โ | โ | โ |
| ๐งช | Test suite | 244 tests | โ | โ | Minimal | โ ๏ธ |
| ๐ง | Actively maintained (2026) | โ | โ | โ | โ ๏ธ Low activity | โ Deprecated |
๐ The Key Differences
validatorsis excellent for format validation (is this a valid URL?), but it's not a security tool. Settingpublic=Trueblocks private IPs, but it won't catch encoded IPs, cloud metadata, path traversal, DNS rebinding, or any of the other 14 attack vectors urlpolice covers.
Pydantic's
HttpUrlvalidates URL structure and integrates beautifully with FastAPI models โ but it performs zero security checks. No SSRF protection, no injection detection, no scheme blocking beyond basic format.
ssrf-protect focuses narrowly on IP-based SSRF. It doesn't handle encoded IPs, DNS rebinding, path traversal, XSS, or encoding bypasses. It also has limited cloud metadata coverage.
SafeURL and Advocate are deprecated and unmaintained. SafeURL had a known SSRF bypass via regex (CVE-2023-24622). Advocate hasn't been updated in years.
urlpolice is the only Python library that treats URL validation as a full security pipeline โ not a single check, but 16+ checks executed in the correct security-critical order, covering the encoding tricks and bypass techniques that appear in real-world CVEs.
๐ฅ Usage Examples
๐ Example 1 โ Webhook Registration
from urlpolice import URLPolice
police = URLPolice.webhook() # HTTPS only, no private IPs, DNS rebinding check
def register_webhook(callback_url: str) -> dict:
result = police.validate(callback_url)
if not result:
return {"error": "Invalid callback URL", "details": result.errors}
save_webhook(result.url) # safe to store and call later
return {"status": "registered"}
๐ฌ Example 2 โ User-Submitted Links
from urlpolice import URLPolice
police = URLPolice.user_content()
urls = [
"https://example.com/article", # โ
valid
"javascript:alert(document.cookie)", # ๐ซ XSS
"http://127.0.0.1:6379/", # ๐ซ SSRF
"https://example.com/../../etc/passwd", # ๐ซ traversal
]
results = police.validate_batch(urls)
safe_urls = [r.url for r in results if r.is_valid]
๐ Example 3 โ Strict API Gateway
from urlpolice import URLPolice
police = URLPolice(
allowed_schemes=frozenset({"https"}),
allowed_domains=frozenset({"api.partner1.com", "api.partner2.com"}),
perform_dns_resolution=True,
)
result = police.validate("https://api.partner1.com/v2/data")
assert result.is_valid # โ
known partner
result = police.validate("https://api.unknown.com/v2/data")
assert not result.is_valid # ๐ซ not in allowlist
โ๏ธ Configuration
All options are set via ValidatorConfig. Pass them as keyword arguments or supply a config object:
from urlpolice import URLPolice, ValidatorConfig
# Keyword arguments
police = URLPolice(allow_private_ips=True, allowed_schemes=frozenset({"https"}))
# Or explicit config object
config = ValidatorConfig(allow_private_ips=True, dns_timeout=3)
police = URLPolice(config=config)
๐ Options Reference
| Option | Type | Default | Description |
|---|---|---|---|
allowed_schemes |
frozenset[str] |
{"http", "https"} |
Permitted URL schemes |
allowed_domains |
frozenset[str] | None |
None |
Domain allowlist (None = all allowed) |
blocked_domains |
frozenset[str] |
set() |
Domains that are always rejected |
allowed_ports |
frozenset[int] | None |
None |
Port allowlist (None = all standard ports) |
allow_private_ips |
bool |
False |
Allow RFC 1918 / reserved addresses |
allow_credentials |
bool |
False |
Allow user:pass@ in the URL |
allow_redirects |
bool |
False |
Allow open-redirect query parameters |
max_url_length |
int |
2048 |
Maximum URL length (DoS prevention) |
max_label_length |
int |
63 |
Maximum DNS label length (RFC 1035) |
perform_dns_resolution |
bool |
True |
Resolve hostnames and validate resolved IPs |
check_dns_rebinding |
bool |
True |
Warn on suspiciously many resolved addresses |
dns_timeout |
int |
5 |
DNS resolution timeout in seconds |
disabled_checks |
frozenset[str] |
set() |
Check names to skip entirely |
๐๏ธ Disabling Specific Checks
police = URLPolice(disabled_checks=frozenset({"homograph", "redirect"}))
Available check names: ssrf ยท scheme ยท credentials ยท redirect ยท xss ยท traversal ยท dns ยท injection ยท homograph ยท encoding ยท ip ยท port
๐ฏ Presets
Four battle-tested presets for the most common scenarios:
from urlpolice import URLPolice
police = URLPolice.strict() # ๐ Production APIs โ maximum security
police = URLPolice.permissive() # ๐ ๏ธ Local development โ everything allowed
police = URLPolice.webhook() # ๐ Webhook callbacks โ HTTPS + DNS check
police = URLPolice.user_content() # ๐ฌ User-submitted links โ balanced safety
| Preset | Schemes | Private IPs | Credentials | Redirects | DNS |
|---|---|---|---|---|---|
| ๐ strict | HTTPS only | โ | โ | โ | โ |
| ๐ ๏ธ permissive | HTTP + HTTPS | โ | โ | โ | โ |
| ๐ webhook | HTTPS only | โ | โ | โ | โ |
| ๐ฌ user_content | HTTP + HTTPS | โ | โ | โ | โ |
๐ File-Based Configuration
Store settings in TOML or JSON โ no code changes needed for different environments:
๐ TOML Example
# urlpolice.toml
[urlpolice]
allowed_schemes = ["https"]
allowed_domains = ["api.example.com", "cdn.example.com"]
blocked_domains = ["evil.com"]
allowed_ports = [443, 8443]
allow_private_ips = false
allow_credentials = false
allow_redirects = false
max_url_length = 2048
perform_dns_resolution = true
check_dns_rebinding = true
disabled_checks = ["homograph"]
๐ JSON Example
{
"urlpolice": {
"allowed_schemes": ["https"],
"allowed_domains": ["api.example.com"],
"blocked_domains": ["evil.com"],
"allowed_ports": [443, 8443],
"allow_private_ips": false
}
}
from urlpolice import URLPolice
police = URLPolice.from_config("urlpolice.toml")
# or
police = URLPolice.from_config("config/security.json")
โ ๏ธ Unknown keys in the config file raise
ConfigurationErrorimmediately โ no silent ignoring.
๐งฉ Individual Checks
Need just one check? Import it directly:
from urlpolice.checks.ssrf import check_ssrf
from urlpolice.checks.traversal import check_traversal
from urlpolice.config import ValidatorConfig
config = ValidatorConfig()
# ๐ Check a hostname for SSRF
result = check_ssrf("192.168.1.1", config)
if result.errors:
print("SSRF risk:", result.errors)
# ๐ Check a path for traversal
result = check_traversal("/../../../etc/passwd")
if result.errors:
print("Traversal attack:", result.errors)
Available modules: ssrf ยท scheme ยท credentials ยท redirect ยท xss ยท traversal ยท dns ยท injection ยท homograph ยท encoding ยท ip ยท port
Each returns a CheckResult(errors=list[str], warnings=list[str]).
๐งช Testing
urlpolice ships with 244 tests covering every check module and integration scenario:
# Run the full suite
pytest
# Verbose output
pytest -v
# With coverage report
pytest --cov=urlpolice --cov-report=term-missing
Development setup:
pip install -e ".[dev]"
pytest
pyproject.tomlalready configurespythonpath = ["src"]โ no manual path hacking needed.
โ FAQ & Troubleshooting
๐ข DNS resolution is slow in tests
Disable it in your test fixtures:
police = URLPolice(perform_dns_resolution=False)
๐ซ Legitimate URLs are being blocked
Inspect result.errors to see which check flagged it. Disable specific checks or use an allowlist:
police = URLPolice(disabled_checks=frozenset({"redirect"}))
# or
police = URLPolice(allowed_domains=frozenset({"trusted.example.com"}))
๐ I need localhost access during development
police = URLPolice.permissive()
# or
police = URLPolice(allow_private_ips=True)
๐ What Python versions are supported?
Python 3.10+. TOML config uses tomllib (3.11+) with automatic tomli fallback for 3.10.
๐ง Can I serialize the config for debugging?
print(police._config.to_dict()) # plain dict, JSON-serializable
๐ค Contributing
Contributions are welcome and appreciated! Here's how to get started:
# Clone the repository
git clone https://github.com/urlpolice/urlpolice.git
cd urlpolice
# Install dev dependencies (uv)
uv sync
# Or with pip
pip install -e ".[dev]"
# Run tests
pytest
# Lint and format
ruff check src/ tests/
ruff format src/ tests/
๐ Guidelines:
- โ Every new check or feature must include tests
- ๐จ Follow existing style โ Ruff-enforced, Google-style docstrings
- ๐งฑ One responsibility per module
- ๐ Security checks err on the side of caution (block by default)
- ๐ฌ Open an issue before starting large changes
๐ License
MIT License โ free for commercial and personal use. See LICENSE for details.
Made with โค๏ธ by the urlpolice contributors
If urlpolice saved your app from an SSRF, give it a โญ โ it helps others find it too.
๐ Homepage โข
๐ Report Bug โข
๐ก Request Feature
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file urlpolice-0.1.2.tar.gz.
File metadata
- Download URL: urlpolice-0.1.2.tar.gz
- Upload date:
- Size: 31.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0e441102d6e4a113f503c8b32984b3ef30e4e32024dbe1f54cb63cebdf222f09
|
|
| MD5 |
1b08c74fbfcc3d2ca064c409531dc096
|
|
| BLAKE2b-256 |
78a6ca4dc29a37475f3a0104649615c0920e0aa7800487d46c9ce0afc11516b2
|
File details
Details for the file urlpolice-0.1.2-py3-none-any.whl.
File metadata
- Download URL: urlpolice-0.1.2-py3-none-any.whl
- Upload date:
- Size: 32.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
91672d635be23a23a94146743567cec5d4b58982b41796fda7d61d763b63a01d
|
|
| MD5 |
2783c5ada9054f22baf908e208d450f9
|
|
| BLAKE2b-256 |
38e8982357605bf960e6e6c03047d2011713558d565348b91a8e7436aba67897
|