flawopen.com/Server-Side Request Forgery/C/C++

Server-Side Request Forgery in C/C++

High Severity CWE-918
ELI5

You send an assistant to buy a newspaper at a store. But the address you gave is 'the safe in the manager's office'. If the assistant doesn't verify that the address is a public shop, he walks into the office and hands you company secrets.

Key terms on this page
IMDSv1 vs IMDSv2
AWS Instance Metadata Service. IMDSv1 responds to simple GET requests at 169.254.169.254; IMDSv2 requires an HTTP PUT session token header.
DNS Rebinding
An attacker provides a domain whose DNS TTL is 0. The first lookup returns a public IP to pass validation, but the second lookup resolves to 127.0.0.1 when the HTTP client connects.

What's happening

When a Python app executes requests.get(user_url), the HTTP client blindly establishes a TCP connection to whatever host the user specifies. Attackers provide loopback addresses (127.0.0.1), internal private ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16), or link-local cloud metadata addresses (169.254.169.254).

Real-world impact

In 2019, the Capital One breach occurred when an attacker exploited an SSRF vulnerability in a WAF running on AWS EC2, querying the IMDS endpoint at 169.254.169.254 to steal IAM role credentials and exfiltrate 100M+ customer records.

CISA Cybersecurity Advisory & MITRE CVE repository.

Vulnerable vs. fixed

VULNERABLE
# VULNERABLE: Naive requests.get to user-provided URL
import requests
from flask import Flask, request, jsonify

app = Flask(__name__)

@app.route('/fetch-avatar')
def fetch_avatar():
    url = request.args.get('url')
    # Attacker passes: http://169.254.169.254/latest/meta-data/iam/security-credentials/
    # Server queries AWS metadata and returns cloud credentials!
    resp = requests.get(url, timeout=5)
    return resp.content, 200, {'Content-Type': resp.headers.get('Content-Type', 'image/png')}
FIXED
# HARDENED: Resolve DNS, parse IP address, deny private/link-local CIDRs
import socket
import ipaddress
import urllib.parse
import requests
from flask import Flask, request, abort

app = Flask(__name__)

BLOCKED_NETWORKS = [
    ipaddress.ip_network('127.0.0.0/8'),      # Loopback
    ipaddress.ip_network('10.0.0.0/8'),       # RFC 1918 Private
    ipaddress.ip_network('172.16.0.0/12'),    # RFC 1918 Private
    ipaddress.ip_network('192.168.0.0/16'),   # RFC 1918 Private
    ipaddress.ip_network('169.254.0.0/16'),   # Link-local / Cloud IMDS
    ipaddress.ip_network('::1/128'),          # IPv6 Loopback
    ipaddress.ip_network('fc00::/7'),         # IPv6 Private ULA
]

def is_safe_ip(ip_str):
    ip = ipaddress.ip_address(ip_str)
    return not any(ip in net for net in BLOCKED_NETWORKS)

@app.route('/fetch-avatar')
def fetch_avatar():
    url = request.args.get('url', '')
    parsed = urllib.parse.urlparse(url)
    
    if parsed.scheme not in ('http', 'https') or not parsed.hostname:
        abort(400, "Invalid URL scheme")
        
    # 1. Resolve all DNS IPs for hostname
    try:
        addr_info = socket.getaddrinfo(parsed.hostname, parsed.port or (443 if parsed.scheme == 'https' else 80))
        resolved_ips = [ai[4][0] for ai in addr_info]
    except socket.gaierror:
        abort(400, "Unable to resolve host")
        
    # 2. Verify all resolved IPs are public
    for ip in resolved_ips:
        if not is_safe_ip(ip):
            abort(403, "Access to internal IP address is prohibited")
            
    # 3. Fetch with redirects disabled to prevent DNS rebinding or redirect bypass
    resp = requests.get(url, timeout=3, allow_redirects=False)
    return resp.content, 200, {'Content-Type': resp.headers.get('Content-Type', 'image/png')}

Why the fix works

The hardened code resolves the hostname to its underlying IP addresses and tests each IP against private, loopback, and link-local CIDR subnets using ipaddress.ip_address(). Setting allow_redirects=False prevents an attacker from supplying an allowed external URL that 302-redirects to an internal address.

Gotchas

DNS Rebinding TOCTOU

Resolving the IP before calling requests.get leaves a microsecond window where DNS could change. For total protection in zero-trust environments, connect directly to the resolved IP using a custom HTTP adapter or proxy.

Octal and Hex IP representations

Attackers use http://0177.0.0.1 or http://2130706433 to evade string filters. ipaddress.ip_address correctly parses all standard integer and octal representations.

Common misconceptions

"Regex checking for '127.0.0.1' is enough"

Regex domain blacklists fail against DNS rebinding, alternative IP notations, and IPv6 equivalents like [::1] or [0:0:0:0:0:ffff:7f00:1].

How to check if you're affected

grep -rn "requests\.get(" --include="*.py" . # Bandit rule B113: bandit -r . -t B113

Prevention checklist

FAQ

Why is 169.254.169.254 so important?

169.254.169.254 is the standard link-local IP used by AWS, Google Cloud, Azure, and OpenStack to serve instance identity, cloud config, and temporary IAM authentication tokens.

References