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.