Skip to main content

Overview

Server-Side Request Forgery (SSRF) is a vulnerability where an attacker can coerce a server-side application to make HTTP requests to an arbitrary location. Instead of attacking the user, the attacker uses the application’s server as a proxy to send crafted requests. This is especially dangerous in cloud environments, as it can be used to access internal-only services or cloud provider metadata endpoints.

Business Impact

A successful SSRF attack can lead to the complete compromise of the server’s cloud account by stealing credentials from metadata services. It allows an attacker to map out and interact with the internal network, bypass firewalls, and access sensitive internal services like databases, admin panels, or internal APIs that are not exposed to the internet.

Reference Details

CWE ID: CWE-918 OWASP Top 10 (2021): A10:2021 - Server-Side Request Forgery Severity: Critical

Framework-Specific Analysis and Remediation

No web framework provides built-in protection against SSRF because making outbound HTTP requests is a fundamental feature. The responsibility lies entirely with the developer to validate and sanitize any user-supplied data that is used to construct the URL for an outbound request. The most robust defense is a strict allow-list of permitted hosts.
  • Python
  • Java
  • .NET(C#)
  • PHP
  • Node.js
  • Ruby

Framework Context

Django applications commonly use the requests library to make outbound HTTP calls. The vulnerability arises when a URL, or part of a URL, is constructed from user input without proper validation.

Vulnerable Scenario 1: Image Importer

A feature allows users to import a profile picture by providing a URL. The server then fetches the image.
# profiles/views.py
import requests
from django.http import HttpResponse

def import_avatar(request):
    image_url = request.GET.get('url')
    # DANGEROUS: The server makes a request to any URL the user provides.
    # An attacker can provide "[http://169.254.169.254/latest/meta-data/iam/security-credentials/ec2-instance-role](http://169.254.169.254/latest/meta-data/iam/security-credentials/ec2-instance-role)"
    # to steal AWS credentials.
    try:
        response = requests.get(image_url, timeout=5)
        # ... process image ...
        return HttpResponse("Image imported.")
    except requests.RequestException:
        return HttpResponse("Could not fetch image.", status=400)

Vulnerable Scenario 2: Webhook Notification

A service allows users to configure a webhook URL to be notified of events.
# webhooks/services.py
def send_webhook_notification(webhook_url, data):
    # DANGEROUS: The server blindly sends a POST request to any configured URL.
    # This can be used to scan internal ports or attack internal services.
    # For example, "http://localhost:8080/admin/delete-all-users"
    requests.post(webhook_url, json=data)

Mitigation and Best Practices

Parse the user-provided URL, extract the hostname, and validate it against a strict allow-list of trusted domains. Never make a request to an IP address directly.

Secure Code Example

# profiles/views.py (Secure Version)
import requests
from urllib.parse import urlparse

ALLOWED_IMAGE_HOSTS = {
    'images.unsplash.com',
    'pbs.twimg.com',
    'cloudinary.com'
}

def import_avatar(request):
    image_url = request.GET.get('url')
    try:
        parsed_url = urlparse(image_url)
        # 1. Check scheme
        if parsed_url.scheme not in ['http', 'https']:
            return HttpResponse("Invalid URL scheme.", status=400)
        
        # 2. Check hostname against allow-list
        if parsed_url.hostname not in ALLOWED_IMAGE_HOSTS:
            return HttpResponse("Host not allowed.", status=400)

        # SAFE: The request is only made after validation.
        response = requests.get(image_url, timeout=5)
        # ... process image ...
        return HttpResponse("Image imported.")
    except (ValueError, requests.RequestException):
        return HttpResponse("Invalid URL or could not fetch image.", status=400)

Testing Strategy

Write tests that attempt to request URLs pointing to localhost, internal IP ranges (e.g., 127.0.0.1, 10.0.0.1, 192.168.1.1), and the cloud metadata service IP (169.254.169.254). Use a mocking library like responses or unittest.mock to intercept the outbound HTTP request and assert that a request is not made for disallowed hosts.
# profiles/tests.py
import responses

@responses.activate
def test_import_avatar_ssrf_attempt_is_blocked(self):
    # The AWS metadata service IP
    ssrf_payload_url = "[http://169.254.169.254/metadata](http://169.254.169.254/metadata)"
    
    # We don't need to mock the request itself because a properly
    # secured function should never even try to make the call.
    
    response = self.client.get(reverse('import-avatar'), {'url': ssrf_payload_url})
    
    self.assertEqual(response.status_code, 400)
    self.assertIn("Host not allowed", response.content.decode())
    
    # Verify that no outbound HTTP call was actually made.
    self.assertEqual(len(responses.calls), 0)