Merge pull request #67 from cheir-mneme/fix/p3-security

fix(security): P3 security vulnerabilities and mitigations
This commit is contained in:
Cody Kickertz 2025-12-19 12:27:44 -06:00 committed by GitHub
commit b85eb4fcde
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 26 additions and 3 deletions

View file

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

View file

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

View file

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

View file

@ -30,7 +30,17 @@ protected override IEnumerable<string> 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

View file

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