fix(nzbget): preserve Basic auth across same-host non-downgrade redirects#580
fix(nzbget): preserve Basic auth across same-host non-downgrade redirects#580kevinheneveld wants to merge 1 commit into
Conversation
37f4160 to
48f8904
Compare
|
I see in the adapter that it mention using JSON-RPC too but I can't find reference to call made using that instead of XML-RPC, at first glance I would suggest to make sure JSON-RPC is not used or that this change does not applies to it too Also, i believe the auto redirect that strips the authorization does that to prevent credential leakage through malevolent redirects, this change breaks that security right ? Wouldn't it be better to define a custom redirection handler that validates the domain is the same and it's not downgrading (HTTPS -> HTTP) then add back the relevant headers instead ? |
…ects When a reverse proxy in front of NZBGet (Caddy/Nginx/Traefik HTTPS upgrade, trailing-slash normalization, etc.) issues a 30x response, .NET's default HttpClientHandler follows the redirect but strips the Authorization header to prevent credential leakage to unintended hosts. The follow-up request then hits NZBGet without auth and is rejected with 401 Unauthorized — even though the configured credentials are correct. Approach (revised after T4g1's review on Listenarrs#580): * Configure the named "nzbget" HttpClient with AllowAutoRedirect=false and attach a custom NzbgetSafeRedirectHandler in the handler chain. * On 301/302/303/307/308, the handler validates that the redirect target is (a) the same host:port and (b) not an HTTPS->HTTP downgrade. Only then does it re-apply the Authorization header to the next request. * For 307/308 the request method and (buffered) body are preserved per RFC 7231; 301/302/303 become GET as HttpClientHandler would. * On any rule violation (cross-host, downgrade, missing Location, redirect loop), the handler throws a typed NzbgetSafeRedirectException with a descriptive message; TestConnectionAsync surfaces that message verbatim so the user sees the actual misconfiguration instead of a generic "Unauthorized" or "network error". The previous approach (embedding user:pass into the URL via DownloadClientUriBuilder.BuildUri(..., includeCredentials: true)) is reverted. URL-embedded credentials would survive a redirect to *any* host, defeating the redirect-strip security control rather than implementing it correctly. The Authorization header is now the sole auth path; NZBGet's WebServer accepts it natively (Authorization: Basic — see daemon/remote/WebServer.cpp). The handler is wired into both Program.cs and the equivalent ServiceRegistrationExtensions registration, so it covers every NZBGet code path that uses the named "nzbget" HttpClient — including FetchDownloadsAsync which calls /jsonrpc (the JSON-RPC gap T4g1 flagged in the same review). Tests rewritten: * DoesNotEmbedCredentialsInUrl — locks in that UserInfo is empty * SendsBasicAuthorizationHeader — unchanged: header is the auth path * ReAppliesAuthHeaderOnSameHostRedirect — [Theory] over 301/302/303/307/308 * BlocksCrossHostRedirectWithClearError — surfaces the descriptive message * BlocksHttpsToHttpDowngradeRedirectWithClearError — same * BailsOutOnRedirectLoopWithClearError — same Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
48f8904 to
bd2e80b
Compare
|
Thanks @T4g1 — both points were on target and I've pivoted the PR accordingly. Quick summary of what changed: On the JSON-RPC code path: you were right that it exists and was missed. On the security concern about URL-embedded creds: also right — Implementation: new PR body and tests rewritten — |
…ects When a reverse proxy in front of NZBGet (Caddy/Nginx/Traefik HTTPS upgrade, trailing-slash normalization, etc.) issues a 30x response, .NET's default HttpClientHandler follows the redirect but strips the Authorization header to prevent credential leakage to unintended hosts. The follow-up request then hits NZBGet without auth and is rejected with 401 Unauthorized — even though the configured credentials are correct. Approach (revised after T4g1's review on Listenarrs#580): * Configure the named "nzbget" HttpClient with AllowAutoRedirect=false and attach a custom NzbgetSafeRedirectHandler in the handler chain. * On 301/302/303/307/308, the handler validates that the redirect target is (a) the same host:port and (b) not an HTTPS->HTTP downgrade. Only then does it re-apply the Authorization header to the next request. * For 307/308 the request method and (buffered) body are preserved per RFC 7231; 301/302/303 become GET as HttpClientHandler would. * On any rule violation (cross-host, downgrade, missing Location, redirect loop), the handler throws a typed NzbgetSafeRedirectException with a descriptive message; TestConnectionAsync surfaces that message verbatim so the user sees the actual misconfiguration instead of a generic "Unauthorized" or "network error". The previous approach (embedding user:pass into the URL via DownloadClientUriBuilder.BuildUri(..., includeCredentials: true)) is reverted. URL-embedded credentials would survive a redirect to *any* host, defeating the redirect-strip security control rather than implementing it correctly. The Authorization header is now the sole auth path; NZBGet's WebServer accepts it natively (Authorization: Basic — see daemon/remote/WebServer.cpp). The handler is wired into both Program.cs and the equivalent ServiceRegistrationExtensions registration, so it covers every NZBGet code path that uses the named "nzbget" HttpClient — including FetchDownloadsAsync which calls /jsonrpc (the JSON-RPC gap T4g1 flagged in the same review). Tests rewritten: * DoesNotEmbedCredentialsInUrl — locks in that UserInfo is empty * SendsBasicAuthorizationHeader — unchanged: header is the auth path * ReAppliesAuthHeaderOnSameHostRedirect — [Theory] over 301/302/303/307/308 * BlocksCrossHostRedirectWithClearError — surfaces the descriptive message * BlocksHttpsToHttpDowngradeRedirectWithClearError — same * BailsOutOnRedirectLoopWithClearError — same Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…ects When a reverse proxy in front of NZBGet (Caddy/Nginx/Traefik HTTPS upgrade, trailing-slash normalization, etc.) issues a 30x response, .NET's default HttpClientHandler follows the redirect but strips the Authorization header to prevent credential leakage to unintended hosts. The follow-up request then hits NZBGet without auth and is rejected with 401 Unauthorized — even though the configured credentials are correct. Approach (revised after T4g1's review on Listenarrs#580): * Configure the named "nzbget" HttpClient with AllowAutoRedirect=false and attach a custom NzbgetSafeRedirectHandler in the handler chain. * On 301/302/303/307/308, the handler validates that the redirect target is (a) the same host:port and (b) not an HTTPS->HTTP downgrade. Only then does it re-apply the Authorization header to the next request. * For 307/308 the request method and (buffered) body are preserved per RFC 7231; 301/302/303 become GET as HttpClientHandler would. * On any rule violation (cross-host, downgrade, missing Location, redirect loop), the handler throws a typed NzbgetSafeRedirectException with a descriptive message; TestConnectionAsync surfaces that message verbatim so the user sees the actual misconfiguration instead of a generic "Unauthorized" or "network error". The previous approach (embedding user:pass into the URL via DownloadClientUriBuilder.BuildUri(..., includeCredentials: true)) is reverted. URL-embedded credentials would survive a redirect to *any* host, defeating the redirect-strip security control rather than implementing it correctly. The Authorization header is now the sole auth path; NZBGet's WebServer accepts it natively (Authorization: Basic — see daemon/remote/WebServer.cpp). The handler is wired into both Program.cs and the equivalent ServiceRegistrationExtensions registration, so it covers every NZBGet code path that uses the named "nzbget" HttpClient — including FetchDownloadsAsync which calls /jsonrpc (the JSON-RPC gap T4g1 flagged in the same review). Tests rewritten: * DoesNotEmbedCredentialsInUrl — locks in that UserInfo is empty * SendsBasicAuthorizationHeader — unchanged: header is the auth path * ReAppliesAuthHeaderOnSameHostRedirect — [Theory] over 301/302/303/307/308 * BlocksCrossHostRedirectWithClearError — surfaces the descriptive message * BlocksHttpsToHttpDowngradeRedirectWithClearError — same * BailsOutOnRedirectLoopWithClearError — same Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Live image bumped to listenarr:local-20260518-0750 (head 40a436b). - PR Listenarrs#580 rewritten on review feedback from T4g1: NzbgetSafeRedirectHandler replaces URL-embedded creds, covers both /xmlrpc and /jsonrpc. - All 9 open upstream PRs are now draft (converted Listenarrs#580, Listenarrs#600, Listenarrs#603). - kevin/live and kevin/live-rebased both carry the new commit. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Live image bumped to listenarr:local-20260518-0750 (head 40a436b). - PR Listenarrs#580 rewritten on review feedback from T4g1: NzbgetSafeRedirectHandler replaces URL-embedded creds, covers both /xmlrpc and /jsonrpc. - All 9 open upstream PRs are now draft (converted Listenarrs#580, Listenarrs#600, Listenarrs#603). - kevin/live and kevin/live-rebased both carry the new commit. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…ects When a reverse proxy in front of NZBGet (Caddy/Nginx/Traefik HTTPS upgrade, trailing-slash normalization, etc.) issues a 30x response, .NET's default HttpClientHandler follows the redirect but strips the Authorization header to prevent credential leakage to unintended hosts. The follow-up request then hits NZBGet without auth and is rejected with 401 Unauthorized — even though the configured credentials are correct. Approach (revised after T4g1's review on Listenarrs#580): * Configure the named "nzbget" HttpClient with AllowAutoRedirect=false and attach a custom NzbgetSafeRedirectHandler in the handler chain. * On 301/302/303/307/308, the handler validates that the redirect target is (a) the same host:port and (b) not an HTTPS->HTTP downgrade. Only then does it re-apply the Authorization header to the next request. * For 307/308 the request method and (buffered) body are preserved per RFC 7231; 301/302/303 become GET as HttpClientHandler would. * On any rule violation (cross-host, downgrade, missing Location, redirect loop), the handler throws a typed NzbgetSafeRedirectException with a descriptive message; TestConnectionAsync surfaces that message verbatim so the user sees the actual misconfiguration instead of a generic "Unauthorized" or "network error". The previous approach (embedding user:pass into the URL via DownloadClientUriBuilder.BuildUri(..., includeCredentials: true)) is reverted. URL-embedded credentials would survive a redirect to *any* host, defeating the redirect-strip security control rather than implementing it correctly. The Authorization header is now the sole auth path; NZBGet's WebServer accepts it natively (Authorization: Basic — see daemon/remote/WebServer.cpp). The handler is wired into both Program.cs and the equivalent ServiceRegistrationExtensions registration, so it covers every NZBGet code path that uses the named "nzbget" HttpClient — including FetchDownloadsAsync which calls /jsonrpc (the JSON-RPC gap T4g1 flagged in the same review). Tests rewritten: * DoesNotEmbedCredentialsInUrl — locks in that UserInfo is empty * SendsBasicAuthorizationHeader — unchanged: header is the auth path * ReAppliesAuthHeaderOnSameHostRedirect — [Theory] over 301/302/303/307/308 * BlocksCrossHostRedirectWithClearError — surfaces the descriptive message * BlocksHttpsToHttpDowngradeRedirectWithClearError — same * BailsOutOnRedirectLoopWithClearError — same Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
bd2e80b to
8830955
Compare
…ects When a reverse proxy in front of NZBGet (Caddy/Nginx/Traefik HTTPS upgrade, trailing-slash normalization, etc.) issues a 30x response, .NET's default HttpClientHandler follows the redirect but strips the Authorization header to prevent credential leakage to unintended hosts. The follow-up request then hits NZBGet without auth and is rejected with 401 Unauthorized — even though the configured credentials are correct. Approach (revised after T4g1's review on Listenarrs#580): * Configure the named "nzbget" HttpClient with AllowAutoRedirect=false and attach a custom NzbgetSafeRedirectHandler in the handler chain. * On 301/302/303/307/308, the handler validates that the redirect target is (a) the same host:port and (b) not an HTTPS->HTTP downgrade. Only then does it re-apply the Authorization header to the next request. * For 307/308 the request method and (buffered) body are preserved per RFC 7231; 301/302/303 become GET as HttpClientHandler would. * On any rule violation (cross-host, downgrade, missing Location, redirect loop), the handler throws a typed NzbgetSafeRedirectException with a descriptive message; TestConnectionAsync surfaces that message verbatim so the user sees the actual misconfiguration instead of a generic "Unauthorized" or "network error". The previous approach (embedding user:pass into the URL via DownloadClientUriBuilder.BuildUri(..., includeCredentials: true)) is reverted. URL-embedded credentials would survive a redirect to *any* host, defeating the redirect-strip security control rather than implementing it correctly. The Authorization header is now the sole auth path; NZBGet's WebServer accepts it natively (Authorization: Basic — see daemon/remote/WebServer.cpp). The handler is wired into both Program.cs and the equivalent ServiceRegistrationExtensions registration, so it covers every NZBGet code path that uses the named "nzbget" HttpClient — including FetchDownloadsAsync which calls /jsonrpc (the JSON-RPC gap T4g1 flagged in the same review). Tests rewritten: * DoesNotEmbedCredentialsInUrl — locks in that UserInfo is empty * SendsBasicAuthorizationHeader — unchanged: header is the auth path * ReAppliesAuthHeaderOnSameHostRedirect — [Theory] over 301/302/303/307/308 * BlocksCrossHostRedirectWithClearError — surfaces the descriptive message * BlocksHttpsToHttpDowngradeRedirectWithClearError — same * BailsOutOnRedirectLoopWithClearError — same Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Live image bumped to listenarr:local-20260518-0750 (head 40a436b). - PR Listenarrs#580 rewritten on review feedback from T4g1: NzbgetSafeRedirectHandler replaces URL-embedded creds, covers both /xmlrpc and /jsonrpc. - All 9 open upstream PRs are now draft (converted Listenarrs#580, Listenarrs#600, Listenarrs#603). - kevin/live and kevin/live-rebased both carry the new commit. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Summary
Adding NZBGet as a download client could fail with
NZBGet XML-RPC error: Unauthorizedeven when the same credentials worked from a manualcurl. The trigger is a reverse proxy in front of NZBGet (Caddy/Nginx/Traefik HTTPS upgrade, trailing-slash normalization, etc.) returning a 30x — .NET's defaultHttpClientHandlerfollows the redirect but stripsAuthorizationto prevent credential leakage to unintended hosts, so NZBGet sees the follow-up without auth and 401s.Approach
Revised after @T4g1's review — the original commit on this branch (URL-embedded
user:pass@host) was abandoned because it would also forward credentials across cross-host or HTTPS→HTTP redirects, defeating the very security control it was working around.The new approach treats this as a redirect-handling concern rather than an auth-encoding concern:
"nzbget"HttpClient is now configured withAllowAutoRedirect = falseand a customNzbgetSafeRedirectHandlerin its delegating-handler chain.Authorizationheader on the next request.HttpClientHandler's defaults.Location, redirect loop), the handler throws a typedNzbgetSafeRedirectExceptionwith a descriptive message.TestConnectionAsyncsurfaces that message verbatim so users see the actual misconfiguration instead of a generic"Authentication failed"/"network error".The handler is wired into both
Program.csand the equivalentServiceRegistrationExtensions.AddListenarrHttpClientsregistration, so every code path that uses the named"nzbget"HttpClient is covered — includingFetchDownloadsAsyncwhich calls/jsonrpc(the JSON-RPC gap T4g1 flagged in the same review).AddViaJsonRpcAsyncdelegates throughCallXmlRpcAsyncand is covered transitively.The Authorization header is now the sole auth path. NZBGet's own
WebServer.cppacceptsAuthorization: Basicnatively (seedaemon/remote/WebServer.cppParseHeaders()), so URL-embedded credentials were never required.Why not let
HttpClientHandlerhandle redirects with its built-in safety?Considered, but
HttpClientHandler.AllowAutoRedirect = trueis exactly what causes the original bug: it stripsAuthorizationon every hop, unconditionally, so any 30x kills auth. The framework has no per-host re-apply hook. ADelegatingHandlerwithAllowAutoRedirect = falseis the established pattern in this codebase (used inTransmissionAdapter,TorrentFileDownloader,ImageCacheService,MyAnonamouseHelper, all with similar comments about preserving auth/cookies across redirects).Test plan
dotnet test --filter "FullyQualifiedName~NzbgetAdapterTests"passes locally (13 tests including new redirect-flow coverage)dotnet test→ 641 passed, 0 failed