aboutsummaryrefslogtreecommitdiff
path: root/Emby.Server.Implementations/Updates/InstallationManager.cs
diff options
context:
space:
mode:
Diffstat (limited to 'Emby.Server.Implementations/Updates/InstallationManager.cs')
-rw-r--r--Emby.Server.Implementations/Updates/InstallationManager.cs97
1 files changed, 70 insertions, 27 deletions
diff --git a/Emby.Server.Implementations/Updates/InstallationManager.cs b/Emby.Server.Implementations/Updates/InstallationManager.cs
index 67b77a112d..6a60f7f5f6 100644
--- a/Emby.Server.Implementations/Updates/InstallationManager.cs
+++ b/Emby.Server.Implementations/Updates/InstallationManager.cs
@@ -1,4 +1,5 @@
using System;
+using System.Buffers;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
@@ -32,6 +33,8 @@ namespace Emby.Server.Implementations.Updates
/// </summary>
public class InstallationManager : IInstallationManager
{
+ private static readonly SearchValues<char> InvalidPackageNameChars = SearchValues.Create([.. Path.GetInvalidFileNameChars(), '/', '\\']);
+
/// <summary>
/// The logger.
/// </summary>
@@ -521,48 +524,68 @@ namespace Emby.Server.Implementations.Updates
return;
}
+ if (!IsValidPackageDirectoryName(package.Name))
+ {
+ _logger.LogError("Refusing to install package with invalid name {PackageName}.", package.Name);
+ throw new InvalidDataException($"Plugin package name '{package.Name}' is not a valid directory name.");
+ }
+
// Always override the passed-in target (which is a file) and figure it out again
string targetDir = Path.Combine(_appPaths.PluginsPath, package.Name);
- using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
- .GetAsync(new Uri(package.SourceUrl), cancellationToken).ConfigureAwait(false);
- response.EnsureSuccessStatusCode();
- await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
-
- // CA5351: Do Not Use Broken Cryptographic Algorithms
-#pragma warning disable CA5351
- cancellationToken.ThrowIfCancellationRequested();
-
- var hash = Convert.ToHexString(await MD5.HashDataAsync(stream, cancellationToken).ConfigureAwait(false));
- if (!string.Equals(package.Checksum, hash, StringComparison.OrdinalIgnoreCase))
+ var pluginsRoot = Path.TrimEndingDirectorySeparator(Path.GetFullPath(_appPaths.PluginsPath));
+ var resolvedTarget = Path.GetFullPath(targetDir);
+ if (!resolvedTarget.StartsWith(pluginsRoot + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase))
{
_logger.LogError(
- "The checksums didn't match while installing {Package}, expected: {Expected}, got: {Received}",
+ "Refusing to install package {PackageName}: resolved target {Resolved} is outside plugins directory {Root}.",
package.Name,
- package.Checksum,
- hash);
- throw new InvalidDataException("The checksum of the received data doesn't match.");
+ resolvedTarget,
+ pluginsRoot);
+ throw new InvalidDataException($"Plugin package name '{package.Name}' resolves outside the plugins directory.");
}
- // Version folder as they cannot be overwritten in Windows.
- targetDir += "_" + package.Version;
-
- if (Directory.Exists(targetDir))
+ using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
+ .GetAsync(new Uri(package.SourceUrl), cancellationToken).ConfigureAwait(false);
+ response.EnsureSuccessStatusCode();
+ Stream stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
+ await using (stream.ConfigureAwait(false))
{
- try
+ // CA5351: Do Not Use Broken Cryptographic Algorithms
+#pragma warning disable CA5351
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var hash = Convert.ToHexString(await MD5.HashDataAsync(stream, cancellationToken).ConfigureAwait(false));
+ if (!string.Equals(package.Checksum, hash, StringComparison.OrdinalIgnoreCase))
{
- Directory.Delete(targetDir, true);
+ _logger.LogError(
+ "The checksums didn't match while installing {Package}, expected: {Expected}, got: {Received}",
+ package.Name,
+ package.Checksum,
+ hash);
+ throw new InvalidDataException("The checksum of the received data doesn't match.");
}
+
+ // Version folder as they cannot be overwritten in Windows.
+ targetDir += "_" + package.Version;
+
+ if (Directory.Exists(targetDir))
+ {
+ try
+ {
+ Directory.Delete(targetDir, true);
+ }
#pragma warning disable CA1031 // Do not catch general exception types
- catch
+ catch
#pragma warning restore CA1031 // Do not catch general exception types
- {
- // Ignore any exceptions.
+ {
+ // Ignore any exceptions.
+ }
}
- }
- stream.Position = 0;
- await ZipFile.ExtractToDirectoryAsync(stream, targetDir, true, cancellationToken);
+ stream.Position = 0;
+ await ZipFile.ExtractToDirectoryAsync(stream, targetDir, true, cancellationToken).ConfigureAwait(false);
+ }
// Ensure we create one or populate existing ones with missing data.
await _pluginManager.PopulateManifest(package.PackageInfo, package.Version, targetDir, status).ConfigureAwait(false);
@@ -570,6 +593,26 @@ namespace Emby.Server.Implementations.Updates
_pluginManager.ImportPluginFrom(targetDir);
}
+ private static bool IsValidPackageDirectoryName(string? name)
+ {
+ if (string.IsNullOrWhiteSpace(name))
+ {
+ return false;
+ }
+
+ if (name.Equals(".", StringComparison.Ordinal) || name.Equals("..", StringComparison.Ordinal))
+ {
+ return false;
+ }
+
+ if (name.IndexOfAny(InvalidPackageNameChars) >= 0)
+ {
+ return false;
+ }
+
+ return true;
+ }
+
private async Task<bool> InstallPackageInternal(InstallationInfo package, CancellationToken cancellationToken)
{
LocalPlugin? plugin = _pluginManager.Plugins.FirstOrDefault(p => p.Id.Equals(package.Id) && p.Version.Equals(package.Version))