---
# === IDENTITY ===
id: software/security/ssrf-prevention/2026
canonical_question: "How do I prevent Server-Side Request Forgery (SSRF)?"
aliases:
  - "SSRF prevention best practices"
  - "server-side request forgery mitigation"
  - "protect against SSRF attacks"
  - "CWE-918 prevention"
  - "block SSRF in cloud environments"
  - "prevent SSRF AWS metadata access"
  - "URL validation to prevent SSRF"
  - "how to stop SSRF in Python Node.js Go Java"
entity_type: software_reference
domain: software > security > SSRF Prevention
region: global
jurisdiction: global
temporal_scope: 2024-2026

# === VERIFICATION ===
last_verified: 2026-02-27
confidence: 0.95
version: 1.0
first_published: 2026-02-27

# === TEMPORAL VALIDITY ===
temporal_validity:
  status: stable
  last_breaking_change: "AWS IMDSv2 enforced by default on new instances (2024); OWASP Top 10 2021 added SSRF as A10"
  next_review: 2026-08-26
  change_sensitivity: low

# === CONSTRAINTS ===
constraints:
  - "NEVER rely solely on blocklists to prevent SSRF -- blocklists are trivially bypassed via IP encoding, DNS rebinding, and URL parser inconsistencies"
  - "ALWAYS validate the resolved IP address after DNS resolution, not just the hostname -- DNS rebinding can change resolution between validation and fetch"
  - "NEVER allow user-controlled URLs to reach cloud metadata endpoints (169.254.169.254, fd00:ec2::254) -- credential theft leads to full cloud account compromise"
  - "Disable HTTP redirects in outbound requests or re-validate the target after each redirect -- open redirects bypass allowlist checks"
  - "NEVER trust user input for URL scheme -- restrict to http and https only to block file://, gopher://, dict:// protocol smuggling"
  - "Network-layer controls (firewall rules, VPC isolation) are REQUIRED in addition to application-layer validation -- defense in depth"

# === SKIP CONDITIONS ===
skip_this_unit_if:
  - condition: "Need to prevent XSS (cross-site scripting, not server-side forgery)"
    use_instead: "software/security/xss-prevention/2026"
  - condition: "Need CSRF protection (cross-site request forgery, client-initiated)"
    use_instead: "software/security/csrf-prevention/2026"
  - condition: "Need general input validation beyond URL handling"
    use_instead: "software/patterns/input-validation/2026"

# === AGENT HINTS ===
inputs_needed:
  - key: "language"
    question: "What programming language or framework is making outbound HTTP requests?"
    type: choice
    options: ["Python", "Node.js", "Go", "Java/Spring", "Ruby", "PHP", "Other"]
  - key: "cloud_provider"
    question: "Is the application running in a cloud environment?"
    type: choice
    options: ["AWS", "GCP", "Azure", "On-premise", "Multiple/Unknown"]

# === DISTRIBUTION ===
canonical_source: "https://knowledgelib.io/software/security/ssrf-prevention/2026"
suggested_citation: "Source: knowledgelib.io -- AI Knowledge Library (verified 2026-02-27)"

# === RELATED UNITS ===
related_kos:
  depends_on: []
  related_to:
    - id: "software/security/xss-prevention/2026"
      label: "XSS Prevention Guide"
    - id: "software/patterns/input-validation/2026"
      label: "Input Validation Patterns"
  solves: []
  alternative_to: []
  often_confused_with:
    - id: "software/security/csrf-prevention/2026"
      label: "CSRF Prevention (client-side forgery, not server-side)"
    - id: "software/security/sql-injection-prevention/2026"
      label: "SQL Injection Prevention"

# === SOURCES (7 authoritative sources) ===
# Types: official_docs, technical_blog, rfc_spec, academic_paper, community_resource, industry_report
# Reliability: high, moderate_high, moderate, moderate_low, low, authoritative
sources:
  - id: src1
    title: "Server Side Request Forgery Prevention Cheat Sheet"
    author: OWASP Foundation
    url: https://cheatsheetseries.owasp.org/cheatsheets/Server_Side_Request_Forgery_Prevention_Cheat_Sheet.html
    type: community_resource
    published: 2024-06-15
    reliability: authoritative
  - id: src2
    title: "What is SSRF (Server-side request forgery)?"
    author: PortSwigger
    url: https://portswigger.net/web-security/ssrf
    type: technical_blog
    published: 2025-01-10
    reliability: high
  - id: src3
    title: "CWE-918: Server-Side Request Forgery (SSRF)"
    author: MITRE
    url: https://cwe.mitre.org/data/definitions/918.html
    type: official_docs
    published: 2024-12-01
    reliability: authoritative
  - id: src4
    title: "A10:2021 -- Server-Side Request Forgery (SSRF)"
    author: OWASP Foundation
    url: https://owasp.org/Top10/A10_2021-Server-Side_Request_Forgery_(SSRF)/
    type: community_resource
    published: 2021-09-01
    reliability: authoritative
  - id: src5
    title: "Defense in Depth Against SSRF with EC2 Instance Metadata Service"
    author: AWS Security Blog
    url: https://aws.amazon.com/blogs/security/defense-in-depth-open-firewalls-reverse-proxies-ssrf-vulnerabilities-ec2-instance-metadata-service/
    type: official_docs
    published: 2024-03-15
    reliability: high
  - id: src6
    title: "Mitigating SSRF Vulnerabilities in Go"
    author: Snyk
    url: https://snyk.io/blog/mitigating-ssrf-vulnerabilities-in-go/
    type: technical_blog
    published: 2024-08-01
    reliability: moderate_high
  - id: src7
    title: "How to Prevent SSRF Attacks in 2025"
    author: Ghost Security
    url: https://ghostsecurity.com/blog/how-to-prevent-ssrf-attacks-in-2025
    type: technical_blog
    published: 2025-01-15
    reliability: moderate_high
---

# SSRF Prevention: Server-Side Request Forgery Defense Guide

## TL;DR

- **Bottom line**: Combine allowlist-based URL validation with DNS resolution checks and network-layer controls (firewall rules, VPC isolation) -- no single layer prevents all SSRF bypass techniques.
- **Key tool/command**: Validate parsed URL host against an allowlist, then verify the resolved IP is not in private/reserved ranges (`10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`, `169.254.169.254`) before making the request.
- **Watch out for**: Blocklist-only approaches -- attackers bypass them via decimal/octal IP encoding, DNS rebinding, URL parser inconsistencies, and HTTP redirects to internal targets.
- **Works with**: All server-side languages and frameworks. Cloud-specific: enforce AWS IMDSv2, GCP metadata `Metadata-Flavor: Google` header, Azure IMDS token requirement.

## Constraints
<!-- Agents: read this section before recommending any action from this unit.
     These are hard constraints that must not be violated. -->

- NEVER rely solely on blocklists to prevent SSRF -- blocklists are trivially bypassed via IP encoding, DNS rebinding, and URL parser inconsistencies
- ALWAYS validate the resolved IP address after DNS resolution, not just the hostname -- DNS rebinding can change resolution between validation and fetch
- NEVER allow user-controlled URLs to reach cloud metadata endpoints (169.254.169.254, fd00:ec2::254) -- credential theft leads to full cloud account compromise
- Disable HTTP redirects in outbound requests or re-validate the target after each redirect -- open redirects bypass allowlist checks
- NEVER trust user input for URL scheme -- restrict to http and https only to block file://, gopher://, dict:// protocol smuggling
- Network-layer controls (firewall rules, VPC isolation) are REQUIRED in addition to application-layer validation -- defense in depth

## Quick Reference

| # | Vulnerability | Risk | Vulnerable Code | Secure Code |
|---|---|---|---|---|
| 1 | Internal network access | Critical -- access admin panels, databases, internal APIs | `requests.get(user_url)` | Allowlist hosts + validate resolved IP is not private |
| 2 | Cloud metadata theft | Critical -- IAM credential exfiltration (Capital One breach) | `fetch(url)` reaching `169.254.169.254` | Block link-local ranges + enforce IMDSv2 (PUT + token) |
| 3 | DNS rebinding | High -- IP changes between validation and request | Validate hostname, then fetch (separate DNS lookups) | Pin DNS resolution: resolve once, validate IP, fetch by IP |
| 4 | Protocol smuggling | High -- file://, gopher://, dict:// read local files or interact with services | `urllib.urlopen(user_url)` with no scheme check | Allowlist only `http://` and `https://` schemes |
| 5 | Open redirect bypass | High -- redirect from allowed domain to internal target | Allow redirects to pre-validated domain | Disable redirects or re-validate after each hop |
| 6 | IP encoding bypass | Medium -- decimal, octal, hex IP representations bypass blocklists | Block `127.0.0.1` string only | Resolve to IP, then check against private ranges |
| 7 | URL parser confusion | Medium -- inconsistent parsing between validator and HTTP client | Validate URL with one parser, fetch with another | Use same URL parsing library for validation and request |
| 8 | IPv6 bypass | Medium -- `::1`, `[::]`, `::ffff:127.0.0.1` bypass IPv4 blocklists | Block only IPv4 loopback | Check both IPv4 and IPv6 private/reserved ranges |
| 9 | Blind SSRF | Medium -- no response returned but side-channel timing/errors leak info | Fetch user URL, discard response (but side effects remain) | Same validation as full SSRF -- response visibility is irrelevant |
| 10 | TOCTOU race condition | Medium -- DNS resolution changes between check and use | Validate, sleep/delay, then fetch | Resolve once, connect directly to resolved IP with Host header |

## Decision Tree

```
START: Does the application need to fetch user-supplied URLs?
|-- Can you restrict to a fixed set of allowed hosts?
|   |-- YES --> Use strict allowlist of hostnames + validate resolved IP
|   |-- NO  |
|-- Does the app run in a cloud environment (AWS/GCP/Azure)?
|   |-- YES --> Block metadata IPs at network layer + enforce IMDSv2/equivalent + app-layer validation
|   |-- NO  |
|-- Does the URL come from user input or external data?
|   |-- YES --> Parse URL, validate scheme (http/https only), resolve DNS,
|   |          check resolved IP against private ranges, disable redirects
|   |-- NO  |
|-- Is the URL from a trusted internal configuration?
|   |-- YES --> Still validate at network layer (defense in depth)
|   |-- NO  |
|-- DEFAULT --> Deny outbound requests by default, allowlist specific destinations
```

## Step-by-Step Guide

### 1. Validate URL scheme and parse safely

Reject any URL that does not use `http` or `https`. Use a well-tested URL parser -- never construct URLs via string concatenation. [src1]

```python
from urllib.parse import urlparse

def validate_url_scheme(url: str) -> bool:
    """Reject non-HTTP(S) schemes to prevent protocol smuggling."""
    try:
        parsed = urlparse(url)
        return parsed.scheme in ('http', 'https') and parsed.hostname is not None
    except Exception:
        return False
```

**Verify**: Test with `file:///etc/passwd`, `gopher://internal:25/`, `dict://localhost:6379/` -- all should be rejected.

### 2. Resolve DNS and validate the IP address

After parsing the URL, resolve the hostname to an IP address and check that it is not in any private, loopback, or link-local range. This prevents DNS rebinding and IP encoding bypasses. [src1]

```python
import ipaddress
import socket

def is_safe_ip(ip_str: str) -> bool:
    """Return True only if the IP is a public, routable address."""
    try:
        ip = ipaddress.ip_address(ip_str)
        return ip.is_global and not ip.is_reserved and not ip.is_loopback \
               and not ip.is_link_local and not ip.is_private
    except ValueError:
        return False

def resolve_and_validate(hostname: str) -> str:
    """Resolve hostname and return IP only if it is safe."""
    results = socket.getaddrinfo(hostname, None)
    for family, _, _, _, sockaddr in results:
        ip_str = sockaddr[0]
        if is_safe_ip(ip_str):
            return ip_str
    raise ValueError(f"Hostname {hostname} resolves to blocked IP range")
```

**Verify**: Test with `localhost`, `169.254.169.254`, `10.0.0.1`, `[::1]` -- all should raise ValueError.

### 3. Disable HTTP redirects and connect by resolved IP

After resolving and validating the IP, make the request directly to the IP with the original Host header. Disable automatic redirect following to prevent redirect-based bypasses. [src2]

```python
import requests

def safe_fetch(url: str, timeout: int = 10) -> requests.Response:
    """Fetch a URL with SSRF protections applied."""
    from urllib.parse import urlparse, urlunparse
    parsed = urlparse(url)

    if not validate_url_scheme(url):
        raise ValueError(f"Blocked scheme: {parsed.scheme}")

    safe_ip = resolve_and_validate(parsed.hostname)

    # Replace hostname with resolved IP, pass original Host header
    safe_url = urlunparse(parsed._replace(netloc=f"{safe_ip}:{parsed.port or 443}"))
    headers = {'Host': parsed.hostname}

    return requests.get(safe_url, headers=headers, timeout=timeout,
                        allow_redirects=False, verify=True)
```

**Verify**: Test with a URL that redirects to `http://169.254.169.254/latest/meta-data/` -- should return the redirect response (3xx) without following it.

### 4. Enforce cloud metadata protections

For AWS, enforce IMDSv2 which requires a PUT request with a TTL header to obtain a session token before metadata access. Block the metadata IP at the network layer as additional defense. [src5]

```bash
# AWS: Require IMDSv2 on all EC2 instances (blocks simple GET-based SSRF)
aws ec2 modify-instance-metadata-options \
  --instance-id i-1234567890abcdef0 \
  --http-tokens required \
  --http-put-response-hop-limit 1 \
  --http-endpoint enabled

# AWS: Block metadata IP at instance level with iptables
iptables -A OUTPUT -d 169.254.169.254 -j DROP

# GCP: Metadata requires Metadata-Flavor: Google header (blocks simple SSRF)
# Azure: IMDS requires Metadata: true header
```

**Verify**: `curl -s http://169.254.169.254/latest/meta-data/` should fail. IMDSv2 test: `curl -s -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600"` should only work from the instance itself with hop limit 1.

### 5. Implement network-layer controls

Apply firewall rules and network policies as defense in depth. Application-layer validation can have bugs; network controls provide a safety net. [src4]

```yaml
# Kubernetes NetworkPolicy: deny egress to private ranges
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: deny-internal-egress
spec:
  podSelector:
    matchLabels:
      app: web-service
  policyTypes:
    - Egress
  egress:
    - to:
        - ipBlock:
            cidr: 0.0.0.0/0
            except:
              - 10.0.0.0/8
              - 172.16.0.0/12
              - 192.168.0.0/16
              - 169.254.0.0/16
              - 127.0.0.0/8
```

**Verify**: From the pod, `curl http://169.254.169.254/` and `curl http://10.0.0.1/` should time out or be refused.

## Code Examples

### Python: URL Validation with DNS Pinning

> Full script: [python-url-validation-with-dns-pinning.py](scripts/python-url-validation-with-dns-pinning.py) (28 lines)

```python
# Input:  User-supplied URL string
# Output: HTTP response or ValueError if URL targets private/reserved IP
import ipaddress, socket, requests
from urllib.parse import urlparse
BLOCKED_RANGES = [
# ... (see full script)
```

### Node.js: SSRF-Safe HTTP Request

> Full script: [node-js-ssrf-safe-http-request.js](scripts/node-js-ssrf-safe-http-request.js) (50 lines)

```javascript
// Input:  User-supplied URL string
// Output: HTTP response or throws Error if URL targets blocked IP
const { URL } = require('url');          // built-in
const dns = require('dns').promises;     // built-in
const https = require('https');          // built-in
# ... (see full script)
```

### Go: URL Validation with IP Check

> Full script: [go-url-validation-with-ip-check.go](scripts/go-url-validation-with-ip-check.go) (50 lines)

```go
// Input:  User-supplied URL string
// Output: *http.Response or error if URL targets private/reserved IP
package ssrf
import (
    "fmt"
# ... (see full script)
```

### Java/Spring: SSRF Protection with InetAddress Validation

> Full script: [java-spring-ssrf-protection-with-inetaddress-valid.java](scripts/java-spring-ssrf-protection-with-inetaddress-valid.java) (36 lines)

```java
// Input:  User-supplied URL string
// Output: ResponseEntity or throws SecurityException if URL targets blocked IP
import java.net.*;
import org.springframework.web.client.RestTemplate;
import org.springframework.http.*;
# ... (see full script)
```

## Anti-Patterns

### Wrong: Blocklist-only approach

```python
# BAD -- blocklist filtering is trivially bypassed
BLOCKED_HOSTS = ['127.0.0.1', 'localhost', '169.254.169.254']

def fetch_url(url):
    from urllib.parse import urlparse
    host = urlparse(url).hostname
    if host in BLOCKED_HOSTS:
        raise ValueError("Blocked host")
    return requests.get(url)  # Bypass: 0x7f000001, 017700000001, 2130706433
```

### Correct: Allowlist with DNS resolution check

```python
# GOOD -- resolve DNS first, then check IP against private ranges
def fetch_url(url):
    parsed = urlparse(url)
    if parsed.scheme not in ('http', 'https'):
        raise ValueError("Blocked scheme")
    for _, _, _, _, sockaddr in socket.getaddrinfo(parsed.hostname, parsed.port):
        if is_ip_blocked(sockaddr[0]):
            raise ValueError(f"Blocked IP: {sockaddr[0]}")
    return requests.get(url, allow_redirects=False, timeout=10)
```

### Wrong: Trusting redirects after initial validation

```python
# BAD -- attacker hosts allowed.com which 302-redirects to http://169.254.169.254/
def fetch_url(url):
    validate_url(url)  # Passes -- allowed.com is on allowlist
    return requests.get(url, allow_redirects=True)  # Follows redirect to metadata!
```

### Correct: Disable redirects or re-validate after each hop

```python
# GOOD -- disable redirects entirely
def fetch_url(url):
    validate_url(url)
    resp = requests.get(url, allow_redirects=False, timeout=10)
    if 300 <= resp.status_code < 400:
        # Optionally: validate redirect target before following
        redirect_url = resp.headers.get('Location')
        validate_url(redirect_url)  # Re-validate the redirect target
        return requests.get(redirect_url, allow_redirects=False, timeout=10)
    return resp
```

### Wrong: No DNS resolution check (vulnerable to DNS rebinding)

```python
# BAD -- hostname resolves to public IP during validation,
# then attacker's DNS rebinds to 169.254.169.254 on the actual request
def fetch_url(url):
    host = urlparse(url).hostname
    ip = socket.gethostbyname(host)  # First resolution: 1.2.3.4 (public)
    if is_private(ip):
        raise ValueError("Blocked")
    return requests.get(url)  # Second resolution: 169.254.169.254 (rebind!)
```

### Correct: Pin DNS resolution and connect by IP

```python
# GOOD -- resolve once, validate, then connect directly to the resolved IP
def fetch_url(url):
    parsed = urlparse(url)
    ip = socket.gethostbyname(parsed.hostname)  # Single resolution
    if is_ip_blocked(ip):
        raise ValueError(f"Blocked IP: {ip}")
    # Connect to the resolved IP directly, with Host header
    safe_url = url.replace(parsed.hostname, ip)
    return requests.get(safe_url, headers={'Host': parsed.hostname},
                        allow_redirects=False, timeout=10, verify=True)
```

## Common Pitfalls

- **IPv4-mapped IPv6 bypass**: Blocklists that only check IPv4 ranges miss `::ffff:127.0.0.1` or `::ffff:169.254.169.254`. Fix: Normalize IPv6-mapped addresses to IPv4 before checking, or use `ipaddress.ip_address()` which handles both. [src1]
- **URL parser inconsistencies**: Different URL parsers interpret ambiguous URLs differently (`http://evil.com#@allowed.com`). Fix: Use the same parsing library for validation and request execution; parse once, re-use the result. [src2]
- **Decimal/octal/hex IP encoding**: `2130706433` (decimal for 127.0.0.1), `0177.0.0.1` (octal), `0x7f000001` (hex) all resolve to localhost but bypass string-based blocklists. Fix: Always resolve to IP, then check against numeric ranges. [src1]
- **TOCTOU on DNS resolution**: Time-of-check to time-of-use gap allows DNS rebinding between validation and fetch. Fix: Resolve DNS once, validate the IP, then connect directly to the IP with a Host header. [src7]
- **Cloud metadata on non-AWS**: GCP metadata at `metadata.google.internal` (169.254.169.254), Azure IMDS at `169.254.169.254` -- both need blocking. Fix: Block the entire `169.254.0.0/16` range regardless of cloud provider. [src5]
- **Incomplete redirect handling**: Following redirects silently can redirect from an allowed domain to `http://169.254.169.254/`. Fix: Either disable redirects entirely (`allow_redirects=False`) or re-validate each redirect destination. [src2]
- **Allowing file:// and gopher:// schemes**: `file:///etc/passwd` reads local files; `gopher://` can interact with Redis, Memcached, SMTP. Fix: Strict allowlist of `http` and `https` only. [src3]
- **Trusting webhook URLs from third parties**: Webhook registration endpoints often accept arbitrary URLs without SSRF validation. Fix: Apply the same SSRF-safe fetch to all outbound HTTP requests, including webhooks and callbacks. [src4]

## Diagnostic Commands

```bash
# Check if cloud metadata endpoint is accessible from an instance
curl -s -o /dev/null -w "%{http_code}" http://169.254.169.254/latest/meta-data/
# Expected: connection refused or timeout (if properly blocked)

# Verify IMDSv2 enforcement on AWS EC2
aws ec2 describe-instances --instance-ids i-1234567890abcdef0 \
  --query 'Reservations[].Instances[].MetadataOptions'
# Expected: HttpTokens: "required"

# Test SSRF protections with various IP encodings
curl -v http://your-app.com/fetch?url=http://2130706433/  # decimal 127.0.0.1
curl -v http://your-app.com/fetch?url=http://0x7f000001/  # hex 127.0.0.1
curl -v http://your-app.com/fetch?url=http://0177.0.0.1/  # octal 127.0.0.1
# Expected: all should be blocked by your application

# Scan for SSRF with OWASP ZAP
docker run -t ghcr.io/zaproxy/zaproxy:stable zap-baseline.py \
  -t https://your-site.com -r report.html

# Find outbound HTTP request code in Python codebase
grep -rn 'requests\.get\|requests\.post\|urlopen\|urllib\.request' --include="*.py" .

# Find outbound HTTP request code in Node.js codebase
grep -rn 'http\.get\|https\.get\|fetch(\|axios\.' --include="*.js" --include="*.ts" .

# Check iptables rules blocking metadata IP
iptables -L OUTPUT -n | grep 169.254
```

## Version History & Compatibility

| Standard/Milestone | Date | Key Change |
|---|---|---|
| OWASP Top 10 A10:2021 | Sep 2021 | SSRF added as its own category (previously bundled under other injection types) |
| AWS IMDSv2 | Nov 2019 | Session-based metadata access; PUT + token requirement |
| AWS IMDSv2 default enforcement | 2024 | New EC2 instances default to IMDSv2-only |
| GCP metadata header requirement | 2020 | `Metadata-Flavor: Google` header required for metadata access |
| Azure IMDS header requirement | 2020 | `Metadata: true` header required for IMDS access |
| CWE-918 | Ongoing | MITRE weakness entry for SSRF classification |
| Capital One breach | Jul 2019 | SSRF + IMDSv1 exploited to steal 100M+ customer records |

## When to Use / When Not to Use

| Use When | Don't Use When | Use Instead |
|---|---|---|
| Application fetches URLs provided by users (webhooks, image imports, link previews) | Application only makes requests to hardcoded internal endpoints | Static configuration with no user input in URLs |
| Accepting webhook callback URLs from third parties | All outbound URLs are compile-time constants | No SSRF risk if no user influence on request targets |
| Running in cloud environments (AWS, GCP, Azure) | On-premise with no cloud metadata endpoints | Still apply private IP blocking but metadata protection is less critical |
| Rendering user-supplied content that may embed URLs (PDF generation, HTML-to-image) | Processing only structured data (JSON/XML) with no URL fields | Input validation for the structured format instead |

## Important Caveats

- DNS rebinding attacks can defeat application-layer validation if DNS resolution and HTTP request happen in separate system calls -- always pin the resolved IP and connect directly
- IMDSv2 is not a complete fix: if the attacker can execute arbitrary HTTP requests (not just GET) from the instance, they can still obtain metadata tokens via PUT
- Network-layer controls (firewall rules, security groups) should be the primary defense; application-layer validation is secondary -- bugs in URL parsing code are common
- Some URL parsing libraries treat `http://127.0.0.1:80@evil.com/` differently -- the authority component parsing varies between implementations
- SSRF protections may break legitimate functionality (webhooks, RSS feeds, oEmbed) -- test thoroughly and provide an allowlist escape hatch for known-good domains
- Server-side PDF/image generation libraries (wkhtmltopdf, Puppeteer, ImageMagick) are common SSRF vectors -- apply the same URL validation to their inputs

## Related Units
<!-- Generated from related_kos frontmatter -->

- [XSS Prevention Guide](/software/security/xss-prevention/2026)
- [Input Validation Patterns](/software/patterns/input-validation/2026)
- [CSRF Prevention](/software/security/csrf-prevention/2026)
- [SQL Injection Prevention](/software/security/sql-injection-prevention/2026)