Skip to main content

Your URL Security Guardian - Protecting Against SSRF, XSS, and 16+ Attack Vectors

Project description

urlpolice logo

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.


PyPI versionย  Python versionsย  CI statusย  Coverageย  Licenseย  Downloads


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: ValidationResult is truthy/falsy โ€” use if 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

validators is excellent for format validation (is this a valid URL?), but it's not a security tool. Setting public=True blocks 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 HttpUrl validates 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 ConfigurationError immediately โ€” 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.toml already configures pythonpath = ["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:

  1. โœ… Every new check or feature must include tests
  2. ๐ŸŽจ Follow existing style โ€” Ruff-enforced, Google-style docstrings
  3. ๐Ÿงฑ One responsibility per module
  4. ๐Ÿ”’ Security checks err on the side of caution (block by default)
  5. ๐Ÿ’ฌ 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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

urlpolice-0.1.2.tar.gz (31.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

urlpolice-0.1.2-py3-none-any.whl (32.8 kB view details)

Uploaded Python 3

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

Hashes for urlpolice-0.1.2.tar.gz
Algorithm Hash digest
SHA256 0e441102d6e4a113f503c8b32984b3ef30e4e32024dbe1f54cb63cebdf222f09
MD5 1b08c74fbfcc3d2ca064c409531dc096
BLAKE2b-256 78a6ca4dc29a37475f3a0104649615c0920e0aa7800487d46c9ce0afc11516b2

See more details on using hashes here.

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

Hashes for urlpolice-0.1.2-py3-none-any.whl
Algorithm Hash digest
SHA256 91672d635be23a23a94146743567cec5d4b58982b41796fda7d61d763b63a01d
MD5 2783c5ada9054f22baf908e208d450f9
BLAKE2b-256 38e8982357605bf960e6e6c03047d2011713558d565348b91a8e7436aba67897

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page