EXERCISE 3 — Vulnerable "fetch preview" feature + 3 independent defences
========================================================================
VULNERABLE FEATURE: "FETCH AND PREVIEW IMAGE FROM URL"
User Story: "As a user, I want to paste the URL of an image and get a preview before uploading it."
Implementation (VULNERABLE):
1. User submits form:
2. User enters URL: e.g., "https://example.com/image.jpg"
3. Server receives the URL.
4. Server code:
```python
import requests
image_url = request.form['image_url']
response = requests.get(image_url) # <-- SSRF VULNERABILITY
preview_html = f''
return render_template('preview.html', image=preview_html)
```
5. Server fetches the URL and returns the content.
ATTACK:
Attacker enters: http://localhost:27017 (MongoDB)
Server tries to fetch it; MongoDB responds with its wire protocol.
Attacker can:
- Infer MongoDB is running (response differs from "host not found").
- Chain with other exploits to query/modify the database.
Or:
Attacker enters: http://169.254.169.254/latest/meta-data/iam/security-credentials/
Server fetches it; returns AWS credentials.
Attacker now has API keys to access AWS.
---
DEFENCE 1: URL VALIDATION / ALLOWLISTING
Principle: explicitly allow only expected domains; block private IPs.
Code (pseudocode):
```python
from urllib.parse import urlparse
import ipaddress
ALLOWED_DOMAINS = ['images.example.com', 'cdn.example.com', 'imgur.com']
BLOCKED_IPS = [
ipaddress.ip_network('127.0.0.0/8'), # localhost
ipaddress.ip_network('169.254.169.254/32'), # AWS metadata
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
]
def validate_image_url(url):
parsed = urlparse(url)
# Check scheme
if parsed.scheme not in ['http', 'https']:
raise ValueError("Only http/https allowed")
# Check hostname
hostname = parsed.hostname
if hostname not in ALLOWED_DOMAINS:
raise ValueError(f"{hostname} not in whitelist")
# Resolve hostname to IP
import socket
try:
ip_str = socket.gethostbyname(hostname)
ip = ipaddress.ip_address(ip_str)
except socket.error:
raise ValueError("DNS resolution failed")
# Check resolved IP
for blocked_range in BLOCKED_IPS:
if ip in blocked_range:
raise ValueError("IP in blocked range")
return url
# In handler:
image_url = request.form['image_url']
validate_image_url(image_url) # Raises if invalid
response = requests.get(image_url)
```
HOW IT PREVENTS THE ATTACK:
- Attacker tries http://localhost:27017: hostname is "localhost", not in
ALLOWED_DOMAINS -> REJECTED.
- Attacker tries http://169.254.169.254/...: hostname is "169.254.169.254", not
whitelisted; or it resolves to 169.254.169.254, which matches BLOCKED_IPS ->
REJECTED.
- Attacker tries http://attacker.com/ (which they control, so it resolves to
their IP 203.0.113.99): hostname "attacker.com" is not in ALLOWED_DOMAINS ->
REJECTED.
LIMITATIONS:
- If attacker owns a whitelisted domain (unlikely), they could still SSRF.
- DNS rebinding (not protected if you don't re-validate the resolved IP).
But for most cases, this is the primary defence.
---
DEFENCE 2: NETWORK FILTERING / EGRESS RULES
Principle: the server's firewall prevents outbound connections to internal IPs.
Even if SSRF is exploited, the firewall blocks it.
Setup (firewall/network layer):
- Server is in a private subnet on a corporate network.
- Egress firewall rules:
```
DENY ALL outbound to 10.0.0.0/8 (private)
DENY ALL outbound to 172.16.0.0/12 (private)
DENY ALL outbound to 192.168.0.0/16 (private)
DENY ALL outbound to 127.0.0.0/8 (localhost)
DENY ALL outbound to 169.254.0.0/16 (metadata)
ALLOW ALL outbound to 0.0.0.0/0 EXCEPT above
```
HOW IT PREVENTS THE ATTACK:
- Attacker exploits SSRF, server tries to fetch http://localhost:27017.
- The HTTP client on the server initiates a TCP connection to 127.0.0.1:27017.
- The kernel/firewall intercepts this and DROPS the connection (DENY rule).
- Connection fails; attacker gets no response.
LIMITATIONS:
- Does not protect against SSRF to external IPs (attacker's own server, third-party APIs).
- Requires network/infrastructure setup; not something the app code does.
- If firewall is misconfigured or the server is in a subnet with lax rules, attack succeeds.
BUT: defence in depth. If URL validation fails, firewall still blocks.
---
DEFENCE 3: LEAST PRIVILEGE / SANDBOXING
Principle: the server's outbound requests come from a separate, restricted service
that has NO access to internal resources.
Architecture:
- Main app server: can't make external HTTP requests.
- Separate "image preview" worker service: handles all image fetches.
- Worker service runs in a network segment with NO access to:
* Internal databases (no routes to database subnet).
* Internal APIs (no routes to admin/internal subnet).
* Metadata services (no routes to 169.254.0.0/16).
- Worker service CAN reach external URLs (internet).
HOW IT PREVENTS THE ATTACK:
- Attacker exploits SSRF in the main app.
- Main app attempts to fetch image; instead of doing it directly, it sends a
message to the worker service: "fetch http://localhost:27017".
- Worker service (running in a sandboxed subnet) tries to reach localhost:27017.
- Worker's network has no route to localhost (it's not the main server; it's
isolated). Connection fails.
- Attacker gets nothing.
LIMITATIONS:
- Complex architecture; adds operational overhead.
- Requires multiple services, separate networking.
- If the worker service is on the SAME network as the main server, it doesn't help.
BUT: strongest defence because the attacker's reach is minimized regardless of
code bugs.
---
HOW EACH DEFENCE INDEPENDENTLY PREVENTS THE ATTACK:
Feature request: "Fetch http://localhost:27017"
With Defence 1 (URL Validation):
✓ Validation code checks: hostname is "localhost" -> BLOCKED before any fetch.
✗ Attacker cannot exploit; no request is made.
With Defence 2 (Network Filtering, even if URL validation is missing):
✗ URL validation is missing; code makes the fetch.
✓ Firewall rule: outbound to 127.0.0.1:27017 is DENIED.
✗ Attacker's request fails at the network layer; no response.
With Defence 3 (Least Privilege, even if the above are missing):
✗ URL validation is missing; code makes the fetch.
✗ Firewall might be lax (e.g., worker is on the same subnet).
✓ Worker service is sandboxed and isolated; has no network path to localhost.
✗ Attacker's request fails because the worker's network is isolated.
DEFENCE IN DEPTH:
All three should be used together. If one fails, the others catch it.
- URL validation: cheapest, catches most attacks, happens first.
- Network filtering: catches bypasses to URL validation.
- Least privilege: catches both.
---
ONE-LINE TAKEAWAY:
Defence 1 (URL validation/allowlist): code-level, cheapest, prevents most attacks.
Defence 2 (network egress filtering): infrastructure-level, catches code bugs.
Defence 3 (sandboxing/least privilege): strongest, isolates the blast radius.
Use all three.