From abfa1bde8b8b4ccc26ce4b7bd9af9b896fcb0f3f Mon Sep 17 00:00:00 2001 From: admin Date: Thu, 18 Dec 2025 10:57:55 -0600 Subject: [PATCH] fix(security): address pre-release security blockers - Reject unknown sender types in certificate validation - Disable auto-redirect in SkyHookProxy to prevent HTTPS downgrade - Use proper JSON serialization in InitializeJsonController - Add whitelist validation for Type.GetType in converters --- .../AutoTagSpecificationConverter.cs | 20 +++++++++++ .../CustomFormatSpecificationConverter.cs | 19 ++++++++++ .../MetadataSource/SkyHook/SkyHookProxy.cs | 16 ++++----- .../X509CertificateValidationService.cs | 3 +- .../Frontend/InitializeJsonController.cs | 35 +++++++++++-------- 5 files changed, 69 insertions(+), 24 deletions(-) diff --git a/src/NzbDrone.Core/Datastore/Converters/AutoTagSpecificationConverter.cs b/src/NzbDrone.Core/Datastore/Converters/AutoTagSpecificationConverter.cs index 5857c73f7b..edaa755b1b 100644 --- a/src/NzbDrone.Core/Datastore/Converters/AutoTagSpecificationConverter.cs +++ b/src/NzbDrone.Core/Datastore/Converters/AutoTagSpecificationConverter.cs @@ -9,6 +9,21 @@ namespace NzbDrone.Core.Datastore.Converters { public class AutoTaggingSpecificationConverter : JsonConverter> { + private static readonly HashSet AllowedSpecificationTypes = new(StringComparer.Ordinal) + { + "YearSpecification", + "TagSpecification", + "StudioSpecification", + "StatusSpecification", + "RuntimeSpecification", + "RootFolderSpecification", + "QualityProfileSpecification", + "OriginalLanguageSpecification", + "MonitoredSpecification", + "KeywordSpecification", + "GenreSpecification" + }; + public override void Write(Utf8JsonWriter writer, List value, JsonSerializerOptions options) { var wrapped = value.Select(x => new SpecificationWrapper @@ -43,6 +58,11 @@ public override List Read(ref Utf8JsonReader reader, reader.Read(); // Move to start of object (stored in this property) ValidateToken(reader, JsonTokenType.StartObject); // Start of specification + if (!AllowedSpecificationTypes.Contains(typename)) + { + throw new JsonException($"Invalid specification type: '{typename}'. Type must be one of the allowed specification types."); + } + var type = Type.GetType($"NzbDrone.Core.AutoTagging.Specifications.{typename}, Radarr.Core", true); var item = (IAutoTaggingSpecification)JsonSerializer.Deserialize(ref reader, type, options); results.Add(item); diff --git a/src/NzbDrone.Core/Datastore/Converters/CustomFormatSpecificationConverter.cs b/src/NzbDrone.Core/Datastore/Converters/CustomFormatSpecificationConverter.cs index 81343219ec..fcaca86dd6 100644 --- a/src/NzbDrone.Core/Datastore/Converters/CustomFormatSpecificationConverter.cs +++ b/src/NzbDrone.Core/Datastore/Converters/CustomFormatSpecificationConverter.cs @@ -9,6 +9,20 @@ namespace NzbDrone.Core.Datastore.Converters { public class CustomFormatSpecificationListConverter : JsonConverter> { + private static readonly HashSet AllowedSpecificationTypes = new(StringComparer.Ordinal) + { + "YearSpecification", + "SourceSpecification", + "LanguageSpecification", + "SizeSpecification", + "IndexerFlagSpecification", + "ResolutionSpecification", + "QualityModifierSpecification", + "ReleaseTitleSpecification", + "ReleaseGroupSpecification", + "EditionSpecification" + }; + public override void Write(Utf8JsonWriter writer, List value, JsonSerializerOptions options) { var wrapped = value.Select(x => new SpecificationWrapper @@ -43,6 +57,11 @@ public override List Read(ref Utf8JsonReader reader, reader.Read(); // Move to start of object (stored in this property) ValidateToken(reader, JsonTokenType.StartObject); // Start of formattag + if (!AllowedSpecificationTypes.Contains(typename)) + { + throw new JsonException($"Invalid specification type: '{typename}'. Type must be one of the allowed specification types."); + } + var type = Type.GetType($"NzbDrone.Core.CustomFormats.{typename}, Radarr.Core", true); var item = (ICustomFormatSpecification)JsonSerializer.Deserialize(ref reader, type, options); results.Add(item); diff --git a/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs b/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs index 6afb732698..45fcb7f6cd 100644 --- a/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs +++ b/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs @@ -63,7 +63,7 @@ public HashSet GetChangedMovies(DateTime startTime) .AddQueryParam("since", startDate) .Build(); - request.AllowAutoRedirect = true; + request.AllowAutoRedirect = false; request.SuppressHttpError = true; var response = _httpClient.Get>(request); @@ -77,7 +77,7 @@ public List GetTrendingMovies() .SetSegment("route", "list/tmdb/trending") .Build(); - request.AllowAutoRedirect = true; + request.AllowAutoRedirect = false; request.SuppressHttpError = true; var response = _httpClient.Get>(request); @@ -91,7 +91,7 @@ public List GetPopularMovies() .SetSegment("route", "list/tmdb/popular") .Build(); - request.AllowAutoRedirect = true; + request.AllowAutoRedirect = false; request.SuppressHttpError = true; var response = _httpClient.Get>(request); @@ -106,7 +106,7 @@ public Tuple> GetMovieInfo(int tmdbId) .Resource(tmdbId.ToString()) .Build(); - httpRequest.AllowAutoRedirect = true; + httpRequest.AllowAutoRedirect = false; httpRequest.SuppressHttpError = true; var httpResponse = _httpClient.Get(httpRequest); @@ -139,7 +139,7 @@ public MovieCollection GetCollectionInfo(int tmdbId) .Resource(tmdbId.ToString()) .Build(); - httpRequest.AllowAutoRedirect = true; + httpRequest.AllowAutoRedirect = false; httpRequest.SuppressHttpError = true; var httpResponse = _httpClient.Get(httpRequest); @@ -172,7 +172,7 @@ public List GetBulkMovieInfo(List tmdbIds) httpRequest.SetContent(tmdbIds.ToJson()); httpRequest.ContentSummary = tmdbIds.ToJson(Formatting.None); - httpRequest.AllowAutoRedirect = true; + httpRequest.AllowAutoRedirect = false; httpRequest.SuppressHttpError = true; var httpResponse = _httpClient.Post>(httpRequest); @@ -201,7 +201,7 @@ public MovieMetadata GetMovieByImdbId(string imdbId) .Resource(imdbId.ToString()) .Build(); - httpRequest.AllowAutoRedirect = true; + httpRequest.AllowAutoRedirect = false; httpRequest.SuppressHttpError = true; var httpResponse = _httpClient.Get>(httpRequest); @@ -524,7 +524,7 @@ public List SearchForNewMovie(string title) .AddQueryParam("year", yearTerm) .Build(); - request.AllowAutoRedirect = true; + request.AllowAutoRedirect = false; request.SuppressHttpError = true; var httpResponse = _httpClient.Get>(request); diff --git a/src/NzbDrone.Core/Security/X509CertificateValidationService.cs b/src/NzbDrone.Core/Security/X509CertificateValidationService.cs index 7efc877ad4..41d1cdf604 100644 --- a/src/NzbDrone.Core/Security/X509CertificateValidationService.cs +++ b/src/NzbDrone.Core/Security/X509CertificateValidationService.cs @@ -24,9 +24,10 @@ public bool ShouldByPassValidationError(object sender, X509Certificate certifica { var targetHostName = string.Empty; + // Security: reject unknown sender types instead of bypassing validation if (sender is not SslStream && sender is not string) { - return true; + return false; } if (sender is SslStream request) diff --git a/src/Radarr.Http/Frontend/InitializeJsonController.cs b/src/Radarr.Http/Frontend/InitializeJsonController.cs index 708859cf60..93a2376a42 100644 --- a/src/Radarr.Http/Frontend/InitializeJsonController.cs +++ b/src/Radarr.Http/Frontend/InitializeJsonController.cs @@ -1,4 +1,4 @@ -using System.Text; +using System.Text.Json; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using NzbDrone.Common.EnvironmentInfo; @@ -19,6 +19,11 @@ public class InitializeJsonController : Controller private static string _urlBase; private string _generatedContent; + private static readonly JsonSerializerOptions JsonOptions = new() + { + WriteIndented = true + }; + public InitializeJsonController(IConfigFileProvider configFileProvider, IAnalyticsService analyticsService) { @@ -42,21 +47,21 @@ private string GetContent() return _generatedContent; } - var builder = new StringBuilder(); - builder.AppendLine("{"); - builder.AppendLine($" \"apiRoot\": \"{_urlBase}/api/v3\","); - builder.AppendLine($" \"apiKey\": \"{_apiKey}\","); - builder.AppendLine($" \"release\": \"{BuildInfo.Release}\","); - builder.AppendLine($" \"version\": \"{BuildInfo.Version.ToString()}\","); - builder.AppendLine($" \"instanceName\": \"{_configFileProvider.InstanceName.ToString()}\","); - builder.AppendLine($" \"theme\": \"{_configFileProvider.Theme.ToString()}\","); - builder.AppendLine($" \"branch\": \"{_configFileProvider.Branch.ToLower()}\","); - builder.AppendLine($" \"analytics\": {_analyticsService.IsEnabled.ToString().ToLowerInvariant()},"); - builder.AppendLine($" \"urlBase\": \"{_urlBase}\","); - builder.AppendLine($" \"isProduction\": {RuntimeInfo.IsProduction.ToString().ToLowerInvariant()}"); - builder.AppendLine("}"); + var config = new + { + apiRoot = $"{_urlBase}/api/v3", + apiKey = _apiKey, + release = BuildInfo.Release, + version = BuildInfo.Version.ToString(), + instanceName = _configFileProvider.InstanceName, + theme = _configFileProvider.Theme.ToString(), + branch = _configFileProvider.Branch.ToLower(), + analytics = _analyticsService.IsEnabled, + urlBase = _urlBase, + isProduction = RuntimeInfo.IsProduction + }; - _generatedContent = builder.ToString(); + _generatedContent = JsonSerializer.Serialize(config, JsonOptions); return _generatedContent; }