Background
Prowlarr is an open-source indexer manager for the *arr ecosystem (Radarr, Sonarr, Lidarr, etc.). It acts as a centralized proxy for torrent and Usenet indexers, so a typical homelab setup has it sitting alongside a media server stack with direct access to download clients and a lot of other internal services.
I was doing a code review of Prowlarr looking for security issues when I spotted a missing validation check in the login controller. The short version: after a successful login, Prowlarr blindly redirects the user to whatever URL was passed in the returnUrl query parameter, including fully-qualified external URLs. That is a textbook open redirect.
This was fixed in PR #2616, merged March 5, 2026. No release containing the fix is available yet. Watch the releases page for the next version.
The Vulnerability
- Title: Open Redirect via Unvalidated
returnUrlon Login - Component:
src/Prowlarr.Http/Authentication/AuthenticationController.cs - Affected versions: All versions prior to the fix in PR #2616
- CVSS 3.0: 6.1 Medium,
CVSS:3.0/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N
The vulnerable code
After a successful login, AuthenticationController.Login() handles the post-auth redirect like this:
if (returnUrl.IsNullOrWhiteSpace())
{
return Redirect(_configFileProvider.UrlBase + "/");
}
if (_configFileProvider.UrlBase.IsNullOrWhiteSpace() || returnUrl.StartsWith(_configFileProvider.UrlBase))
{
return Redirect(returnUrl);
}
return Redirect(_configFileProvider.UrlBase + returnUrl);
The intent is to restrict redirects to paths under the configured UrlBase. The problem is the guard condition: _configFileProvider.UrlBase.IsNullOrWhiteSpace(). Prowlarr’s UrlBase defaults to an empty string, confirmed in ConfigFileProvider.cs:
var urlBase = (_serverOptions.UrlBase ?? GetValue("UrlBase", "")).Trim('/');
GetValue("UrlBase", "") returns "" when no value is configured, which is the case in the vast majority of installs. An empty string passes IsNullOrWhiteSpace(), which short-circuits the entire condition and sends the user wherever returnUrl says, including https://attacker.com.
Attack prerequisites
- Network access to the Prowlarr login page (the attacker never has to touch it themselves)
- Ability to get a link in front of the target user (email, chat, a post, anything)
- Valid credentials for the Prowlarr instance (held by the victim, not required from the attacker)
Proof of Concept
Craft a login URL pointing at Prowlarr with an external returnUrl:
http://<prowlarr-host>:9696/login?returnUrl=https://attacker.example.com
The victim follows the link, sees the real Prowlarr login page, and enters their credentials. Prowlarr authenticates them successfully and issues:
HTTP/1.1 302 Found
Location: https://attacker.example.com
The victim’s browser navigates to the attacker’s site. From the victim’s perspective, they just logged into Prowlarr and ended up somewhere unexpected. There is no visual warning.
Confirming the redirect with curl:
curl http://localhost:9696/login?returnUrl=http://www.attacker.com/malicious_file.html -d "username=admin&password=admin&RememberMe=on" -v
* Host localhost:9696 was resolved.
* IPv6: ::1
* IPv4: 127.0.0.1
* Trying [::1]:9696...
* Established connection to localhost (::1 port 9696) from ::1 port 36672
* using HTTP/1.x
> POST /login?returnUrl=http://www.attacker.com/malicious_file.html HTTP/1.1
> Host: localhost:9696
> User-Agent: curl/8.18.0
> Accept: */*
> Content-Length: 43
> Content-Type: application/x-www-form-urlencoded
>
* upload completely sent off: 43 bytes
< HTTP/1.1 302 Found
< Content-Length: 0
< Date: Sun, 22 Feb 2026 00:51:50 GMT
< Server: Kestrel
< Cache-Control: no-cache,no-store
< Expires: Thu, 01 Jan 1970 00:00:00 GMT
< Last-Modified: Fri, 14 Nov 2025 23:13:29 GMT
< Location: http://www.attacker.com/malicious_file.html
< Pragma: no-cache
< Set-Cookie: ProwlarrAuth=CfDJ8C1CQNp7UDRMpAzlWiRffcqE-rugo0xWRNQA2dsjrpP5Yx5na4e62_UrTK2JHlayUnbE0A02ylxMwq_3TWwbqcxI7mNICcdzcKW1cTukRcGgOIMrF0JKE0jZkFB6HblOVzJsPc9-eK4a32rEUVsPAHpv9Vh8EGgoj1RxmBwUOnNbtM79j4jJjcBmQdbYXiB5ziOrSTIkefhLcsRMH0u5B1frbdIRdHnP3URXL3hQHWcOfmudvuzjWoUoosm3-aU4NcspcaMN-vTrmMNOFuyHhu98hVpDSJWxd7HO_FNl0U58Dy8s-fagbpxaAgPA1KRlXukwcOajUJrSNaB9xnTIONvV1d1QKT9HTu2KcujzmdkPbvJGXWsAtCwjnxT5IryqbC9GGZ2nmP2dFa5w6hBBBRyzvTIlBLzhJQqBT0Pp1GQQ_4HX5UHfiX5SDQ8hdm2ncA; expires=Sun, 01 Mar 2026 00:51:50 GMT; path=/; samesite=lax; httponly
<
* Connection #0 to host localhost:9696 left intact
Why This Matters
Prowlarr is typically deployed on a home server or NAS alongside Radarr, Sonarr, and download clients. It holds API keys for every connected application and credentials for every indexer. The admin who runs it is exactly the kind of target worth phishing.
A few concrete scenarios:
- Credential harvesting. Redirect to a page that mimics the Prowlarr UI and prompts the user to “log in again due to a session error.” Users who just authenticated are primed to re-enter credentials without suspicion.
- Malware delivery. Redirect to a page that serves a fake Prowlarr update or a malicious browser extension.
- Social engineering leverage. Use the legitimate Prowlarr domain in the visible link (
http://your-prowlarr/login?...) to make the phishing URL look trustworthy in previews and link-checkers.
The attack requires no access to Prowlarr at all from the attacker’s side, which keeps the footprint minimal.
The Fix
PR #2616 adds a single condition to the first branch of the redirect logic:
// Before
if (returnUrl.IsNullOrWhiteSpace())
// After
if (returnUrl.IsNullOrWhiteSpace() || !Url.IsLocalUrl(returnUrl))
Url.IsLocalUrl() is ASP.NET Core’s built-in helper. It returns false for any absolute URL (anything with a scheme, like https://attacker.com, or protocol-relative like //attacker.com). When returnUrl is not local, the code now falls into the safe branch and redirects to the default post-login path instead.
The fix itself was cherry-picked from Sonarr commit 14005d8, where the same bug was fixed back in August 2024. Prowlarr and the rest of the *arr applications share a significant amount of code, but this particular fix did not make it across for over a year.
Timeline
| Date | Event |
|---|---|
| 2026-02-21 | Vulnerability discovered during code review |
| 2026-02-21 | Reported to Prowlarr via GitHub Security Advisory |
| 2026-03-05 | Fix merged in PR #2616 (no release available yet) |
| 2026-03-05 | Public disclosure |
Takeaways
This one is a good reminder that security fixes in shared codebases do not automatically propagate to sibling projects. The *arr family (Sonarr, Radarr, Lidarr, Readarr, Prowlarr, Whisparr) all share the same authentication code. Sonarr had the right behavior for sixteen months before Prowlarr picked it up. If you are maintaining a fork or a project with shared upstream code, it is worth periodically diffing your security-relevant files against the upstream to catch fixes you may have missed.
The underlying pattern is also common: a guard condition that is supposed to restrict a redirect instead silently allows everything through when a configuration value is at its default. The IsNullOrWhiteSpace() check on UrlBase was written to handle the “no base path configured” case for the path-prefix logic, but it accidentally became a bypass for the entire origin check. Subtle enough to miss in review, easy to exploit once you spot it.
All testing was performed against a local Prowlarr instance. The vulnerability was reported to the Prowlarr team before publication.