Back to Blog
critical SEVERITY7 min read

How Server-Side Request Forgery happens in Python FastAPI and how to fix it

A critical Server-Side Request Forgery (SSRF) vulnerability was discovered in app.py where the `/parse` and `/parse-video` endpoints accepted user-supplied URLs with only substring validation. The application checked if 'doubao.com' appeared anywhere in the URL string, allowing attackers to bypass this check and access internal services, cloud metadata endpoints, or scan the internal network. The fix implemented proper hostname parsing with an allowlist of legitimate domains.

O
By Orbis AppSec
Published September 1, 2026Reviewed September 1, 2026

Answer Summary

This is a Server-Side Request Forgery (SSRF) vulnerability (CWE-918) in a Python FastAPI application. The `/parse` and `/parse-video` endpoints in app.py performed weak URL validation using substring matching (`if "doubao.com" in str(request.url)`), which attackers could bypass with URLs like `http://169.254.169.254/doubao.com` to access AWS metadata or internal services. The fix uses proper hostname parsing with `urlparse()` and validates against an explicit allowlist of domains using the `_host_matches()` helper function, ensuring only legitimate doubao.com and qianwen.com hosts are accepted.

Vulnerability at a Glance

cweCWE-918
fixImplemented hostname parsing with explicit domain allowlist validation
riskAttackers can access internal services, cloud metadata endpoints, and scan internal networks
languagePython (FastAPI)
root causeSubstring matching instead of proper hostname validation in URL processing
vulnerabilityServer-Side Request Forgery (SSRF)

Introduction

In a FastAPI application serving as a parser API for Doubao and Qianwen conversation links, we discovered a critical Server-Side Request Forgery (SSRF) vulnerability in app.py at line 70. The /parse and /parse-video endpoints accepted user-supplied URLs and made HTTP requests to fetch content, but the validation logic used a dangerously weak approach: checking if the string 'doubao.com' appeared anywhere in the URL. This seemingly simple oversight created a severe security risk that could allow attackers to access internal AWS metadata endpoints, scan the internal network, or interact with backend services never intended to be publicly accessible.

The vulnerable code in the parse_doubao() function looked innocuous at first glance, but the substring check if "doubao.com" in str(request.url) was fundamentally flawed and easily bypassed.

The Vulnerability Explained

Let's examine the vulnerable code from app.py at line 83:

@app.post("/parse", summary="解析豆包|千问对话图片")
async def parse_doubao(request: DouBaoRequest):
    try:
        if "doubao.com" in str(request.url):
            result = await doubao_image_parse(str(request.url), return_raw=request.return_raw)
        else:
            result = await qianwen_image_parse(str(request.url), return_raw=request.return_raw)

The problem lies in line 83: if "doubao.com" in str(request.url). This validation only checks if the string "doubao.com" appears anywhere in the URL—not whether it's actually the hostname being accessed. This is a classic SSRF vulnerability pattern.

How Could This Be Exploited?

An attacker could craft malicious URLs that pass this validation check but actually target internal resources:

Attack Scenario 1: AWS Metadata Endpoint Access

# Attacker sends this URL:
"http://169.254.169.254/latest/meta-data/iam/security-credentials/doubao.com"

# The check passes because "doubao.com" is in the string
# But the actual HTTP request goes to AWS metadata endpoint
# Attacker retrieves IAM credentials, API keys, and cloud secrets

Attack Scenario 2: Internal Service Scanning

# Attacker sends:
"http://internal-database:5432/admin?ref=doubao.com"

# Again, the check passes
# The doubao_image_parse() function makes a request to the internal database
# Attacker can probe internal network topology and services

Attack Scenario 3: Localhost Exploitation

# Attacker sends:
"http://localhost:8080/admin/delete-all?doubao.com"

# The substring check passes
# Request hits local administrative endpoints
# Attacker can trigger dangerous operations on the server itself

Real-World Impact

In this specific FastAPI application, the SSRF vulnerability could have allowed attackers to:

  1. Extract cloud credentials: Access AWS EC2 metadata at http://169.254.169.254/ to retrieve IAM role credentials, SSH keys, and environment variables
  2. Map internal infrastructure: Probe internal IP ranges to discover databases, cache servers, and microservices
  3. Bypass authentication: Access internal-only admin panels or APIs that don't require authentication when accessed from localhost
  4. Chain attacks: Use the SSRF as a pivot point to exploit other vulnerabilities in internal services

The doubao_image_parse() and qianwen_image_parse() functions would dutifully make HTTP requests to whatever URL was provided, effectively turning the server into an attack proxy.

The Fix

The security fix implemented proper hostname validation using Python's urlparse() module. Here's the before and after comparison:

Before (Vulnerable Code):

if "doubao.com" in str(request.url):
    result = await doubao_image_parse(str(request.url), return_raw=request.return_raw)
else:
    result = await qianwen_image_parse(str(request.url), return_raw=request.return_raw)

After (Secure Code):

from urllib.parse import urlparse

ALLOWED_DOUBAO_HOSTS = {"doubao.com", "www.doubao.com"}
ALLOWED_QIANWEN_HOSTS = {"qianwen.com", "www.qianwen.com"}

def _host_matches(url: str, allowed_hosts: set[str]) -> bool:
    hostname = (urlparse(url).hostname or "").lower()
    return hostname in allowed_hosts or any(hostname.endswith(f".{host}") for host in allowed_hosts)

# In the endpoint:
url_str = str(request.url)
if _host_matches(url_str, ALLOWED_DOUBAO_HOSTS):
    result = await doubao_image_parse(url_str, return_raw=request.return_raw)
elif _host_matches(url_str, ALLOWED_QIANWEN_HOSTS):
    result = await qianwen_image_parse(url_str, return_raw=request.return_raw)
else:
    raise HTTPException(status_code=400, detail="不支持的链接域名")

How This Fix Solves the Problem

The new _host_matches() helper function implements several critical security improvements:

  1. Proper hostname extraction: urlparse(url).hostname extracts only the actual hostname from the URL, ignoring paths, query parameters, and fragments
  2. Explicit allowlist: The ALLOWED_DOUBAO_HOSTS and ALLOWED_QIANWEN_HOSTS sets define exactly which domains are permitted
  3. Subdomain support: The check hostname.endswith(f".{host}") allows legitimate subdomains like api.doubao.com while rejecting malicious-site.com/doubao.com
  4. Case normalization: .lower() prevents bypass attempts using mixed-case hostnames
  5. Fail-secure default: If the hostname doesn't match either allowlist, the endpoint raises an HTTP 400 error with a clear message

Now, the malicious URLs from our attack scenarios are properly rejected:

# These URLs are now blocked:
_host_matches("http://169.254.169.254/doubao.com", ALLOWED_DOUBAO_HOSTS)  # False
_host_matches("http://internal-db:5432?ref=doubao.com", ALLOWED_DOUBAO_HOSTS)  # False
_host_matches("http://localhost/admin?doubao.com", ALLOWED_DOUBAO_HOSTS)  # False

# Only legitimate URLs pass:
_host_matches("https://doubao.com/chat/123", ALLOWED_DOUBAO_HOSTS)  # True
_host_matches("https://api.doubao.com/v1/parse", ALLOWED_DOUBAO_HOSTS)  # True

The same validation pattern was applied to the GET endpoint at line 104, ensuring consistent security across all entry points.

Prevention & Best Practices

To prevent SSRF vulnerabilities in your FastAPI (and other Python web) applications:

1. Always Use Proper URL Parsing

Never validate URLs with string operations like in, .contains(), or regex patterns. Use urllib.parse.urlparse():

from urllib.parse import urlparse

def validate_url(url: str, allowed_hosts: set[str]) -> bool:
    parsed = urlparse(url)
    hostname = (parsed.hostname or "").lower()
    return hostname in allowed_hosts

2. Implement Explicit Allowlists

Define exactly which hosts your application should connect to:

# Good: Explicit allowlist
ALLOWED_HOSTS = {"api.example.com", "cdn.example.com"}

# Bad: Blocklist approach (easy to bypass)
BLOCKED_HOSTS = {"localhost", "127.0.0.1"}  # Incomplete!

3. Validate Protocol and Port

Restrict which protocols and ports are allowed:

def validate_url_strict(url: str) -> bool:
    parsed = urlparse(url)
    if parsed.scheme not in {"https", "http"}:
        return False
    if parsed.port and parsed.port not in {80, 443}:
        return False
    return True

4. Block Private IP Ranges

Even with hostname validation, resolve the hostname and check if it points to private IP space:

import ipaddress
import socket

def is_private_ip(hostname: str) -> bool:
    try:
        ip = ipaddress.ip_address(socket.gethostbyname(hostname))
        return ip.is_private or ip.is_loopback or ip.is_link_local
    except (socket.gaierror, ValueError):
        return True  # Fail secure if resolution fails

5. Use HTTP Client Libraries with SSRF Protection

Consider using libraries that provide built-in SSRF protection:

import httpx

# Configure client with timeout and redirect limits
client = httpx.AsyncClient(
    timeout=10.0,
    max_redirects=3,
    follow_redirects=True
)

6. Implement Defense in Depth

Layer multiple validation checks:

async def safe_fetch(url: str, allowed_hosts: set[str]) -> str:
    # 1. Validate hostname against allowlist
    if not _host_matches(url, allowed_hosts):
        raise ValueError("Host not allowed")

    # 2. Check protocol
    parsed = urlparse(url)
    if parsed.scheme not in {"https", "http"}:
        raise ValueError("Invalid protocol")

    # 3. Resolve and check IP
    if is_private_ip(parsed.hostname):
        raise ValueError("Private IP not allowed")

    # 4. Make request with timeout
    async with httpx.AsyncClient(timeout=10.0) as client:
        response = await client.get(url)
        return response.text

Security Standards References

This vulnerability maps to:
- CWE-918: Server-Side Request Forgery (SSRF)
- OWASP Top 10 2021: A10:2021 – Server-Side Request Forgery
- OWASP ASVS 4.0: V12.6 SSRF Protection Requirements

Key Takeaways

  • Never use substring matching for URL validation: The pattern if "doubao.com" in url is fundamentally insecure and can be bypassed by placing the trusted string in paths, query parameters, or fragments
  • The _host_matches() function is the critical fix: It uses urlparse(url).hostname to extract only the actual hostname, then validates it against an explicit allowlist of permitted domains
  • Fail-secure with explicit rejection: The new code raises HTTPException(status_code=400, detail="不支持的链接域名") when URLs don't match either allowlist, making the security boundary clear
  • Apply validation consistently: Both the POST endpoint at line 80 and GET endpoint at line 104 now use the same _host_matches() validation, preventing inconsistencies
  • Subdomain handling matters: The fix correctly allows subdomains like api.doubao.com through hostname.endswith(f".{host}") while still blocking malicious domains

How Orbis AppSec Detected This

  • Source: User-supplied URL parameter in the DouBaoRequest model accepted by the /parse POST endpoint
  • Sink: HTTP request made by doubao_image_parse() and qianwen_image_parse() functions in app.py:85 and app.py:87
  • Missing control: No proper hostname validation—only substring matching that could be bypassed
  • CWE: CWE-918 (Server-Side Request Forgery)
  • Fix: Implemented _host_matches() helper using urlparse() to extract hostname and validate against explicit allowlists ALLOWED_DOUBAO_HOSTS and ALLOWED_QIANWEN_HOSTS

Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.

Conclusion

The SSRF vulnerability in app.py demonstrates how seemingly simple validation logic can create critical security holes. The substring check if "doubao.com" in str(request.url) looked reasonable at first glance but failed to account for how URLs actually work. By implementing proper hostname parsing with urlparse() and validating against explicit allowlists, the fix ensures that only legitimate Doubao and Qianwen domains can be accessed through the /parse and /parse-video endpoints.

This vulnerability serves as a reminder that security validation must be precise and comprehensive. When your application makes HTTP requests based on user input, always extract and validate the actual hostname, use explicit allowlists, and consider blocking private IP ranges. The defense-in-depth approach—combining multiple layers of validation—provides the most robust protection against SSRF attacks.

References

Frequently Asked Questions

What is Server-Side Request Forgery (SSRF)?

SSRF is a vulnerability where an attacker tricks a server into making HTTP requests to unintended destinations, typically internal services or cloud metadata endpoints that should not be accessible externally.

How do you prevent SSRF in Python FastAPI?

Use proper URL parsing with `urlparse()` to extract the hostname, validate against an explicit allowlist of allowed domains, reject private IP ranges, and never rely on substring matching for URL validation.

What CWE is Server-Side Request Forgery?

SSRF is classified as CWE-918: Server-Side Request Forgery. It's related to CWE-918's parent category CWE-441 (Unintended Proxy or Intermediary).

Is checking for a domain substring enough to prevent SSRF?

No. Substring matching can be easily bypassed with URLs like `http://internal-service/trusted-domain.com` or `http://169.254.169.254/?doubao.com`, where the trusted string appears in the path or query parameters instead of the hostname.

Can static analysis detect SSRF vulnerabilities?

Yes. Modern static analysis tools can detect SSRF by tracking user-controlled data flow into HTTP request functions and identifying missing or weak URL validation patterns, as demonstrated by the multi_agent_ai scanner that flagged this issue.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #47

Related Articles

critical

How Server-Side Request Forgery happens in Node.js maintenance scripts and how to fix it

A critical Server-Side Request Forgery (SSRF) vulnerability was discovered in `maintenance/getImages.js`, where the `getImage()` function passed database-sourced URLs directly to `axios.get()` without any validation. An attacker who could modify the elements database could redirect these requests to internal network resources — including AWS cloud metadata endpoints — potentially exposing IAM credentials and other sensitive infrastructure data. The fix introduces a strict URL allowlist that limi

high

How SSRF via inconsistent IP address parsing happens in Node.js dependencies and how to fix it

A high-severity flaw (CVE-2026-69192) in the widely-used `ip-address` npm package meant that IP strings could be parsed inconsistently compared to the OS resolver and Node's own networking stack — letting an attacker slip a private/loopback address past an allowlist that used `Address4`/`Address6` for validation. This PR pins and upgrades `ip-address` from `10.1.0` to `10.3.1` in both `package.json` (via `overrides`) and `package-lock.json`, eliminating the parser divergence across the whole dep

high

How Octal IP Address Parsing Leads to SSRF in Node.js and How to Fix It

CVE-2026-69192 reveals a critical inconsistency in the `ip-address` library where Address4 decodes leading-zero octets as decimal while DNS resolvers interpret them as octal, creating a dangerous parsing divergence. This mismatch allows attackers to bypass IP-based access controls and perform Server-Side Request Forgery (SSRF) attacks. The fix upgrades `ip-address` from 9.0.5 to 10.3.1, aligning parsing behavior with standard resolver implementations.

critical

How Server-Side Request Forgery (SSRF) happens in Node.js fetch wrappers and how to fix it

A critical Server-Side Request Forgery (SSRF) vulnerability was discovered in the `recon.mjs` script, where a fetch wrapper accepted arbitrary URLs without validation. This allowed attackers to access internal infrastructure and cloud metadata services. The fix implements comprehensive URL validation that blocks internal IP ranges, loopback addresses, and dangerous protocols before any network request is made.

high

How IP Address Parsing Inconsistencies Cause SSRF and Trust-Boundary Bypass in Node.js Applications

The `ip-address` library version 10.2.0 contained a critical parsing inconsistency where the `Address4` decoder interpreted leading-zero octets as decimal numbers, while most DNS resolvers and network systems interpreted them as octal. This mismatch allowed attackers to bypass IP-based access controls and SSRF filters. Upgrading to version 10.3.1 fixes this vulnerability by aligning the library's parsing behavior with standard resolver behavior.

critical

How API Key Exposure and Unsafe Process Spawning Happens in Node.js Scripts and How to Fix It

A critical security vulnerability in the `scripts/close-issues.mjs` file exposed API key patterns in documentation and used unsafe `spawnSync` calls to execute curl commands. The fix replaces dangerous process spawning with native `fetch()` API calls and removes sensitive configuration examples from documentation, eliminating both credential exposure and command injection risks.