From 019f0862b32f38334f47e07ba83a191aa1d9fe49 Mon Sep 17 00:00:00 2001 From: admin Date: Fri, 19 Dec 2025 12:12:54 -0600 Subject: [PATCH] fix(security): address P3 vulnerabilities and add mitigations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Security fixes: - XXE prevention: disable XmlResolver in UTorrentProxy.cs (#42) - Path traversal: validate paths in LogFileController.cs (#44) - Path traversal: validate paths in MediaCoverController.cs (#44) - ReDoS mitigation: add 5s timeout to user regex patterns Documentation: - CORS: document security rationale in Startup.cs (#43) Closes #42, #43, #44 Related: #59, #60, #61 (SonarCloud triage - GitHub alerts now at 0 open) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../Download/Clients/uTorrent/UTorrentProxy.cs | 1 + .../Profiles/Releases/PerlRegexFactory.cs | 3 ++- src/NzbDrone.Host/Startup.cs | 4 ++++ src/Radarr.Api.V3/Logs/LogFileController.cs | 12 +++++++++++- .../MediaCovers/MediaCoverController.cs | 9 ++++++++- 5 files changed, 26 insertions(+), 3 deletions(-) diff --git a/src/NzbDrone.Core/Download/Clients/uTorrent/UTorrentProxy.cs b/src/NzbDrone.Core/Download/Clients/uTorrent/UTorrentProxy.cs index 5c2ee50a61..195e5864e3 100644 --- a/src/NzbDrone.Core/Download/Clients/uTorrent/UTorrentProxy.cs +++ b/src/NzbDrone.Core/Download/Clients/uTorrent/UTorrentProxy.cs @@ -263,6 +263,7 @@ private void AuthenticateClient(HttpRequestBuilder requestBuilder, UTorrentSetti _logger.Debug("uTorrent authentication succeeded."); var xmlDoc = new System.Xml.XmlDocument(); + xmlDoc.XmlResolver = null; // Disable external entity resolution (XXE prevention) xmlDoc.LoadXml(response.Content); authToken = xmlDoc.FirstChild.FirstChild.InnerText; diff --git a/src/NzbDrone.Core/Profiles/Releases/PerlRegexFactory.cs b/src/NzbDrone.Core/Profiles/Releases/PerlRegexFactory.cs index b75e80db94..40b3276ea8 100644 --- a/src/NzbDrone.Core/Profiles/Releases/PerlRegexFactory.cs +++ b/src/NzbDrone.Core/Profiles/Releases/PerlRegexFactory.cs @@ -26,7 +26,8 @@ public static Regex CreateRegex(string pattern, string modifiers) var options = GetOptions(modifiers); // For now we simply expect the pattern to be .net compliant. We should probably check and reject perl-specific constructs. - return new Regex(pattern, options | RegexOptions.Compiled); + // Use timeout to mitigate ReDoS attacks from malicious patterns + return new Regex(pattern, options | RegexOptions.Compiled, TimeSpan.FromSeconds(5)); } private static RegexOptions GetOptions(string modifiers) diff --git a/src/NzbDrone.Host/Startup.cs b/src/NzbDrone.Host/Startup.cs index e2b2d5a8ab..550ea754ff 100644 --- a/src/NzbDrone.Host/Startup.cs +++ b/src/NzbDrone.Host/Startup.cs @@ -73,6 +73,10 @@ public void ConfigureServices(IServiceCollection services) services.AddResponseCompression(options => options.EnableForHttps = true); + // CORS is permissive because: + // 1. All API endpoints require authentication (API key or session) + // 2. Single-user self-hosted model - no cross-user attack surface + // 3. Restrictive CORS would break mobile apps and browser extensions services.AddCors(options => { options.AddPolicy(VersionedApiControllerAttribute.API_CORS_POLICY, diff --git a/src/Radarr.Api.V3/Logs/LogFileController.cs b/src/Radarr.Api.V3/Logs/LogFileController.cs index 3f4be17aad..9a87a41c39 100644 --- a/src/Radarr.Api.V3/Logs/LogFileController.cs +++ b/src/Radarr.Api.V3/Logs/LogFileController.cs @@ -30,7 +30,17 @@ protected override IEnumerable GetLogFiles() protected override string GetLogFilePath(string filename) { - return Path.Combine(_appFolderInfo.GetLogFolder(), filename); + var logFolder = Path.GetFullPath(_appFolderInfo.GetLogFolder()); + var filePath = Path.GetFullPath(Path.Combine(logFolder, filename)); + + // Prevent path traversal - ensure path stays within log folder + if (!filePath.StartsWith(logFolder + Path.DirectorySeparatorChar) && + !filePath.Equals(logFolder, global::System.StringComparison.Ordinal)) + { + return null; + } + + return filePath; } protected override string DownloadUrlRoot diff --git a/src/Radarr.Api.V3/MediaCovers/MediaCoverController.cs b/src/Radarr.Api.V3/MediaCovers/MediaCoverController.cs index cfc2dbb4ea..7412f35877 100644 --- a/src/Radarr.Api.V3/MediaCovers/MediaCoverController.cs +++ b/src/Radarr.Api.V3/MediaCovers/MediaCoverController.cs @@ -28,7 +28,14 @@ public MediaCoverController(IAppFolderInfo appFolderInfo, IDiskProvider diskProv [HttpGet(@"{movieId:int}/{filename:regex((.+)\.(jpg|png|gif))}")] public IActionResult GetMediaCover(int movieId, string filename) { - var filePath = Path.Combine(_appFolderInfo.GetAppDataPath(), "MediaCover", movieId.ToString(), filename); + var mediaCoverPath = Path.GetFullPath(Path.Combine(_appFolderInfo.GetAppDataPath(), "MediaCover")); + var filePath = Path.GetFullPath(Path.Combine(mediaCoverPath, movieId.ToString(), filename)); + + // Prevent path traversal - ensure path stays within MediaCover folder + if (!filePath.StartsWith(mediaCoverPath + Path.DirectorySeparatorChar)) + { + return NotFound(); + } if (!_diskProvider.FileExists(filePath) || _diskProvider.GetFileSize(filePath) == 0) {