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.

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.

Prevention and further reading

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #47

Related Articles

medium

How gitlab.bandit.B501 happens in Python and how to fix it

The `proverbia-scraper.py` script disabled TLS certificate verification on its `requests.get()` call and silenced the resulting security warnings, exposing the scraper to man-in-the-middle attacks. The fix removes the `verify=False` flag and the warning suppression, restoring proper certificate validation while keeping the existing 30-second timeout intact.

high

How Server-Side Request Forgery (SSRF) happens in Go HTTP handlers and how to fix it

A Server-Side Request Forgery (SSRF) vulnerability was discovered in `internal/web/controller/server.go` where the `applySubTemplate` endpoint accepted arbitrary URLs from user input and passed them directly to `serverService.ApplySubTemplateFromGithub()` without any host validation. An attacker could exploit this to make the server issue HTTP requests to internal network resources, cloud metadata endpoints, or redirect-controlled destinations. The fix introduces a strict allowlist that restrict

critical

How SSRF via Vulnerable Dependency Versions Happens in Node.js and How to Fix It

A permissive semver range in `package.json` allowed npm to install axios versions vulnerable to SSRF (CVE-2024-39338). By bumping the minimum version from `^1.6.0` to `^1.7.4`, all downstream consumers of this SDK are now protected from server-side request forgery attacks. This critical fix required changing just one line in the dependency manifest.

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

critical

ExternalHttpClient::request() Sent Basic Auth Over Plain HTTP

The `ExternalHttpClient::request()` helper accepted a `$basicAuth` string and passed it straight to the HTTP client's `auth` option without checking that the target URL used `https://`. Any external JSON data source configured with an `http://` endpoint therefore shipped a base64-encoded `Authorization: Basic` header in cleartext on every scheduled load. The fix rejects the request outright — before a client is even created — when the URL scheme is not HTTPS.