Merge pull request #15 from cheir-mneme/fix/security-pre-release

fix(security): address pre-release security blockers
This commit is contained in:
Cody Kickertz 2025-12-18 11:49:49 -06:00 committed by GitHub
commit 5aa0ebf35d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 69 additions and 24 deletions

View file

@ -9,6 +9,21 @@ namespace NzbDrone.Core.Datastore.Converters
{
public class AutoTaggingSpecificationConverter : JsonConverter<List<IAutoTaggingSpecification>>
{
private static readonly HashSet<string> AllowedSpecificationTypes = new HashSet<string>(StringComparer.Ordinal)
{
"YearSpecification",
"TagSpecification",
"StudioSpecification",
"StatusSpecification",
"RuntimeSpecification",
"RootFolderSpecification",
"QualityProfileSpecification",
"OriginalLanguageSpecification",
"MonitoredSpecification",
"KeywordSpecification",
"GenreSpecification"
};
public override void Write(Utf8JsonWriter writer, List<IAutoTaggingSpecification> value, JsonSerializerOptions options)
{
var wrapped = value.Select(x => new SpecificationWrapper
@ -43,6 +58,11 @@ public override List<IAutoTaggingSpecification> 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);

View file

@ -9,6 +9,20 @@ namespace NzbDrone.Core.Datastore.Converters
{
public class CustomFormatSpecificationListConverter : JsonConverter<List<ICustomFormatSpecification>>
{
private static readonly HashSet<string> AllowedSpecificationTypes = new HashSet<string>(StringComparer.Ordinal)
{
"YearSpecification",
"SourceSpecification",
"LanguageSpecification",
"SizeSpecification",
"IndexerFlagSpecification",
"ResolutionSpecification",
"QualityModifierSpecification",
"ReleaseTitleSpecification",
"ReleaseGroupSpecification",
"EditionSpecification"
};
public override void Write(Utf8JsonWriter writer, List<ICustomFormatSpecification> value, JsonSerializerOptions options)
{
var wrapped = value.Select(x => new SpecificationWrapper
@ -43,6 +57,11 @@ public override List<ICustomFormatSpecification> 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);

View file

@ -63,7 +63,7 @@ public HashSet<int> GetChangedMovies(DateTime startTime)
.AddQueryParam("since", startDate)
.Build();
request.AllowAutoRedirect = true;
request.AllowAutoRedirect = false;
request.SuppressHttpError = true;
var response = _httpClient.Get<List<int>>(request);
@ -77,7 +77,7 @@ public List<MovieMetadata> GetTrendingMovies()
.SetSegment("route", "list/tmdb/trending")
.Build();
request.AllowAutoRedirect = true;
request.AllowAutoRedirect = false;
request.SuppressHttpError = true;
var response = _httpClient.Get<List<MovieResource>>(request);
@ -91,7 +91,7 @@ public List<MovieMetadata> GetPopularMovies()
.SetSegment("route", "list/tmdb/popular")
.Build();
request.AllowAutoRedirect = true;
request.AllowAutoRedirect = false;
request.SuppressHttpError = true;
var response = _httpClient.Get<List<MovieResource>>(request);
@ -106,7 +106,7 @@ public Tuple<MovieMetadata, List<Credit>> GetMovieInfo(int tmdbId)
.Resource(tmdbId.ToString())
.Build();
httpRequest.AllowAutoRedirect = true;
httpRequest.AllowAutoRedirect = false;
httpRequest.SuppressHttpError = true;
var httpResponse = _httpClient.Get<MovieResource>(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<CollectionResource>(httpRequest);
@ -172,7 +172,7 @@ public List<MovieMetadata> GetBulkMovieInfo(List<int> tmdbIds)
httpRequest.SetContent(tmdbIds.ToJson());
httpRequest.ContentSummary = tmdbIds.ToJson(Formatting.None);
httpRequest.AllowAutoRedirect = true;
httpRequest.AllowAutoRedirect = false;
httpRequest.SuppressHttpError = true;
var httpResponse = _httpClient.Post<List<MovieResource>>(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<List<MovieResource>>(httpRequest);
@ -524,7 +524,7 @@ public List<Movie> SearchForNewMovie(string title)
.AddQueryParam("year", yearTerm)
.Build();
request.AllowAutoRedirect = true;
request.AllowAutoRedirect = false;
request.SuppressHttpError = true;
var httpResponse = _httpClient.Get<List<MovieResource>>(request);

View file

@ -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)

View file

@ -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 JsonSerializerOptions()
{
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;
}