flawopen.com/ssrf/Python

● CWE-918 · उच्च
सुरक्षा अनुसंधान · FlawOpen

सुरक्षा भेद्यता: Server-Side Request Forgery in Python

How requests.get allows attackers to query AWS IMDS and internal microservices, and how to implement safe DNS resolution and IP validation.

💡 आसान भाषा में (ELI5)

कल्पना करें कि आप एक सहायक को सार्वजनिक दुकान से पार्सल लाने के लिए भेजते हैं, लेकिन उसे प्रबंधक के कार्यालय में रखी बंद तिजोरी का पता देते हैं। चूंकि सहायक के पास आंतरिक सुरक्षा बैज है, वह तिजोरी खोलकर कंपनी के रहस्य आपको सौंप देता है।

इस पेज के मुख्य शब्द

Web Application Security
सुरक्षा अवधारणा (Web Application Security): Core architecture component affected by CWE-918.
CWE-918
सुरक्षा अवधारणा (CWE-918): Standard Common Weakness Enumeration classification for ssrf-python.
Defense-in-Depth
सुरक्षा अवधारणा (Defense-in-Depth): Multi-layered engineering verification and runtime boundary isolation.

हमले का चरण-दर-चरण प्रवाह

Step 1

अविश्वसनीय रिमोट URL इनपुट

एप्लिकेशन इमेज फ़ेचिंग, वेबहुक या पीडीएफ़ जनरेशन के लिए उपयोगकर्ता द्वारा दिया गया URL स्वीकार करता है।

Step 2

आंतरिक नेटवर्क एड्रेसिंग

हमलावर क्लाउड मेटाडेटा सेवा (उदा. http://169.254.169.254/latest/meta-data/) या लूपबैक एड्रेस इनपुट करता है।

Step 3

अनियंत्रित सॉकेट आउटबाउंड कनेक्शन

सर्वर बिना आईपी सत्यापन के आंतरिक वीपीसी नेटवर्क से अनुरोध भेज देता है।

Step 4

क्लाउड क्रेडेंशियल चोरी

आंतरिक मेटाडेटा सेवा सर्वर के अनुरोध पर विश्वास करके अस्थायी आईएएम टोकन और गुप्त कुंजियाँ लौटा देती है।

सोर्स कोड: कमज़ोर बनाम सुरक्षित कार्यान्वयन

✕ कमज़ोर कार्यान्वयन
# 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')}
✓ सुरक्षित और सुदृढ़ फ़िक्स
# 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')}

इंजीनियरिंग और सिस्टम सुरक्षा चेकलिस्ट

References