Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
<PackageVersion Include="DiscUtils.Iso9660" Version="0.16.13" />
<PackageVersion Include="lzfse-net" Version="1.0.15" />
<PackageVersion Include="lzo.net" Version="0.0.6" />
<PackageVersion Include="Tmds.DBus.Protocol" Version="0.21.3" />
<PackageVersion Include="xunit" Version="2.9.3" />
<PackageVersion Include="xunit.runner.visualstudio" Version="2.8.2" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
Expand Down
2 changes: 2 additions & 0 deletions XcpNgCenter.Shell/XcpNgCenter.Shell.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,7 @@
<PackageReference Include="Avalonia.Desktop" />
<PackageReference Include="Avalonia.Themes.Fluent" />
<PackageReference Include="CommunityToolkit.Mvvm" />
<!-- Pin patched transitive dep from Avalonia.Desktop (CVE / GHSA-xrw6-gwf8-vvr9). -->
<PackageReference Include="Tmds.DBus.Protocol" />
</ItemGroup>
</Project>
115 changes: 13 additions & 102 deletions XcpNgCenter.Shell/packages.lock.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,15 @@
"resolved": "8.4.0",
"contentHash": "tqVU8yc/ADO9oiTRyTnwhFN68hCwvkliMierptWOudIAvWY1mWCh5VFh+guwHJmpMwfg0J0rY+yyd5Oy7ty9Uw=="
},
"Tmds.DBus.Protocol": {
"type": "Direct",
"requested": "[0.21.3, )",
"resolved": "0.21.3",
"contentHash": "hDwB8WsQoyALQKqIbwzS68UKdlnafDm4T/DkO/JrA/YIneP/rKv96SxYPVXeh3FP4i/SXfShrYftKLtciJAIlw==",
"dependencies": {
"System.IO.Pipelines": "8.0.0"
}
},
"Avalonia.Angle.Windows.Natives": {
"type": "Transitive",
"resolved": "2.1.22045.20230930",
Expand Down Expand Up @@ -228,14 +237,6 @@
"resolved": "5.0.0",
"contentHash": "t0MGLukB5WAVU9bO3MGzvlGnyJPgUlcwerXn1kzBRjwLKixT96XV0Uza41W49gVd8zEMFu9vQEFlv0IOrytICA=="
},
"Tmds.DBus.Protocol": {
"type": "Transitive",
"resolved": "0.20.0",
"contentHash": "2gkt2kuYPhDKd8gtl34jZSJOnn4nRJfFngCDcTZT/uySbK++ua0YQx2418l9Rn1Y4dE5XNq6zG9ZsE5ltLlNNw==",
"dependencies": {
"System.IO.Pipelines": "8.0.0"
}
},
"commandlib": {
"type": "Project"
},
Expand All @@ -257,16 +258,16 @@
"xenmodel": {
"type": "Project",
"dependencies": {
"CommandLib": "[0.0.0.26202, )",
"CommandLib": "[2026.8.3, )",
"DiscUtils.Iso9660": "[0.16.13, )",
"DiscUtils.Vhd": "[0.16.13, )",
"DiscUtils.Vhdx": "[0.16.13, )",
"DiscUtils.Vmdk": "[0.16.13, )",
"DiscUtils.Wim": "[0.16.13, )",
"Newtonsoft.Json": "[13.0.4, )",
"System.Drawing.Common": "[8.0.0, )",
"XenCenterLib": "[0.0.0.26202, )",
"XenOvfApi": "[0.0.0.26202, )",
"XenCenterLib": "[2026.8.3, )",
"XenOvfApi": "[2026.8.3, )",
"lzfse-net": "[1.0.15, )",
"lzo.net": "[0.0.6, )"
}
Expand All @@ -275,7 +276,7 @@
"type": "Project",
"dependencies": {
"System.Management": "[8.0.0, )",
"XenCenterLib": "[0.0.0.26202, )"
"XenCenterLib": "[2026.8.3, )"
}
},
"DiscUtils.Iso9660": {
Expand Down Expand Up @@ -390,96 +391,6 @@
"resolved": "8.0.0",
"contentHash": "+TUFINV2q2ifyXauQXRwy4CiBhqvDEDZeVJU7qfxya4aRYOKzVBpN+4acx25VcPB9ywUN6C0n8drWl110PhZEg=="
}
},
"net8.0/linux-x64": {
"Avalonia.Angle.Windows.Natives": {
"type": "Transitive",
"resolved": "2.1.22045.20230930",
"contentHash": "Bo3qOhKC1b84BIhiogndMdAzB3UrrESKK7hS769f5HWeoMw/pcd42US5KFYW2JJ4ZSTrXnP8mXwLTMzh+S+9Lg=="
},
"Avalonia.Native": {
"type": "Transitive",
"resolved": "11.2.5",
"contentHash": "zubOZRBqOETn9uque9pEmUERIasSRL/Va4HQ+dtd+uA+2ThVL9Jo+yp3+NeV2qwrLUcAA/uNd9C+WmG4qB6MvA==",
"dependencies": {
"Avalonia": "11.2.5"
}
},
"HarfBuzzSharp.NativeAssets.Linux": {
"type": "Transitive",
"resolved": "7.3.0.3",
"contentHash": "hkcHeTfOyIeJuPtO/QfoqkDvV/MXebZYaA/Bn/S+nXsjH3Wt9oQ6okH2kklYO+1UUdBSJFd67bi9IrpQXI2mPw==",
"dependencies": {
"HarfBuzzSharp": "7.3.0.3"
}
},
"HarfBuzzSharp.NativeAssets.macOS": {
"type": "Transitive",
"resolved": "7.3.0.3",
"contentHash": "UAwIYnkbBTzBJv1Id8FijY/i8QiIepRemSXufU8fyzwWhYJdx4+ajG8yQUie5HW/uusbVLFSr26muSlJOFDgSw=="
},
"HarfBuzzSharp.NativeAssets.Win32": {
"type": "Transitive",
"resolved": "7.3.0.3",
"contentHash": "RPxRXD16KtSs8Yxr2RK9Qs7AwyN9MlpqZIYs0AvfaJwl7RAtVhC0+u2f2SKwX0uMYYd3O98Z+OBA1sj6aWVKQA=="
},
"Microsoft.Win32.SystemEvents": {
"type": "Transitive",
"resolved": "8.0.0",
"contentHash": "9opKRyOKMCi2xJ7Bj7kxtZ1r9vbzosMvRrdEhVhDz8j8MoBGgB+WmC94yH839NPH+BclAjtQ/pyagvi/8gDLkw=="
},
"SkiaSharp.NativeAssets.Linux": {
"type": "Transitive",
"resolved": "2.88.9",
"contentHash": "cWSaJKVPWAaT/WIn9c8T5uT/l4ETwHxNJTkEOtNKjphNo8AW6TF9O32aRkxqw3l8GUdUo66Bu7EiqtFh/XG0Zg==",
"dependencies": {
"SkiaSharp": "2.88.9"
}
},
"SkiaSharp.NativeAssets.macOS": {
"type": "Transitive",
"resolved": "2.88.9",
"contentHash": "Nv5spmKc4505Ep7oUoJ5vp3KweFpeNqxpyGDWyeEPTX2uR6S6syXIm3gj75dM0YJz7NPvcix48mR5laqs8dPuA=="
},
"SkiaSharp.NativeAssets.Win32": {
"type": "Transitive",
"resolved": "2.88.9",
"contentHash": "wb2kYgU7iy84nQLYZwMeJXixvK++GoIuECjU4ECaUKNuflyRlJKyiRhN1MAHswvlvzuvkrjRWlK0Za6+kYQK7w=="
},
"System.Diagnostics.EventLog": {
"type": "Transitive",
"resolved": "8.0.0",
"contentHash": "fdYxcRjQqTTacKId/2IECojlDSFvp7LP5N78+0z/xH7v/Tuw5ZAxu23Y6PTCRinqyu2ePx+Gn1098NC6jM6d+A=="
},
"System.Security.AccessControl": {
"type": "Transitive",
"resolved": "5.0.0",
"contentHash": "dagJ1mHZO3Ani8GH0PHpPEe/oYO+rVdbQjvjJkBRNQkX4t0r1iaeGn8+/ybkSLEan3/slM0t59SVdHzuHf2jmw==",
"dependencies": {
"Microsoft.NETCore.Platforms": "5.0.0",
"System.Security.Principal.Windows": "5.0.0"
}
},
"System.Security.Principal.Windows": {
"type": "Transitive",
"resolved": "5.0.0",
"contentHash": "t0MGLukB5WAVU9bO3MGzvlGnyJPgUlcwerXn1kzBRjwLKixT96XV0Uza41W49gVd8zEMFu9vQEFlv0IOrytICA=="
},
"lzfse-net": {
"type": "CentralTransitive",
"requested": "[1.0.15, )",
"resolved": "1.0.15",
"contentHash": "O5AhPNxnNhI9Ena8b+VJ1hhgNac8rRmd1rnYopSEWHetU/1Z5xOFPuWT4nxpy2u5Qk7evS5kFj1dRDhcArBtpg=="
},
"System.Management": {
"type": "CentralTransitive",
"requested": "[8.0.0, )",
"resolved": "8.0.0",
"contentHash": "jrK22i5LRzxZCfGb+tGmke2VH7oE0DvcDlJ1HAKYU8cPmD8XnpUT0bYn2Gy98GEhGjtfbR/sxKTVb+dE770pfA==",
"dependencies": {
"System.CodeDom": "8.0.0"
}
}
}
}
}
128 changes: 128 additions & 0 deletions XenCenterLib.Tests/ArchivePathTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
using System;
using System.IO;
using System.IO.Compression;
using System.Text;
using XenCenterLib.Archive;
using Xunit;

namespace XenCenterLib.Tests
{
public class ArchivePathTests
{
[Fact]
public void GetSafeExtractPath_NullOrEmptyDestination_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() => ArchivePath.GetSafeExtractPath(null, "file.txt"));
Assert.Throws<ArgumentNullException>(() => ArchivePath.GetSafeExtractPath("", "file.txt"));
}

[Fact]
public void GetSafeExtractPath_NullOrEmptyEntryName_ThrowsInvalidDataException()
{
Assert.Throws<InvalidDataException>(() => ArchivePath.GetSafeExtractPath(Path.GetTempPath(), null));
Assert.Throws<InvalidDataException>(() => ArchivePath.GetSafeExtractPath(Path.GetTempPath(), ""));
}

[Theory]
[InlineData("../evil.txt")]
[InlineData("..\\evil.txt")]
[InlineData("safe/../../evil.txt")]
[InlineData("/tmp/evil.txt")]
public void GetSafeExtractPath_RejectsTraversalOrRootedPaths(string entryName)
{
var destination = Path.Combine(Path.GetTempPath(), "xcpng-archive-safe-" + Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(destination);
try
{
Assert.Throws<InvalidDataException>(() => ArchivePath.GetSafeExtractPath(destination, entryName));
}
finally
{
Directory.Delete(destination, true);
}
}

[Fact]
public void GetSafeExtractPath_AllowsNestedRelativePaths()
{
var destination = Path.Combine(Path.GetTempPath(), "xcpng-archive-safe-" + Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(destination);
try
{
var resolved = ArchivePath.GetSafeExtractPath(destination, "nested/file.txt");
Assert.StartsWith(Path.GetFullPath(destination), Path.GetFullPath(resolved));
Assert.EndsWith(Path.Combine("nested", "file.txt"), resolved);
}
finally
{
Directory.Delete(destination, true);
}
}

[Fact]
public void ExtractAllContents_RejectsZipSlipEntries()
{
var destination = Path.Combine(Path.GetTempPath(), "xcpng-archive-extract-" + Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(destination);

try
{
using (var zipStream = new MemoryStream())
{
using (var zip = new ZipArchive(zipStream, ZipArchiveMode.Create, true))
{
var entry = zip.CreateEntry("../evil.txt");
using (var writer = new StreamWriter(entry.Open(), Encoding.UTF8))
writer.Write("should-not-extract");
}

zipStream.Position = 0;
using (var iterator = new ZipArchiveIterator(zipStream))
{
Assert.Throws<InvalidDataException>(() => iterator.ExtractAllContents(destination));
}
}

Assert.Empty(Directory.GetFiles(destination, "*", SearchOption.AllDirectories));
}
finally
{
if (Directory.Exists(destination))
Directory.Delete(destination, true);
}
}

[Fact]
public void ExtractAllContents_WritesSafeNestedEntries()
{
var destination = Path.Combine(Path.GetTempPath(), "xcpng-archive-extract-" + Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(destination);

try
{
using (var zipStream = new MemoryStream())
{
using (var zip = new ZipArchive(zipStream, ZipArchiveMode.Create, true))
{
var entry = zip.CreateEntry("nested/ok.txt");
using (var writer = new StreamWriter(entry.Open(), Encoding.UTF8))
writer.Write("ok");
}

zipStream.Position = 0;
using (var iterator = new ZipArchiveIterator(zipStream))
iterator.ExtractAllContents(destination);
}

var extracted = Path.Combine(destination, "nested", "ok.txt");
Assert.True(File.Exists(extracted));
Assert.Equal("ok", File.ReadAllText(extracted));
}
finally
{
if (Directory.Exists(destination))
Directory.Delete(destination, true);
}
}
}
}
3 changes: 3 additions & 0 deletions XenCenterLib.Tests/XenCenterLib.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,7 @@
<ItemGroup>
<ProjectReference Include="..\XenCenterLib\XenCenterLib.csproj" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net481'">
<Reference Include="System.IO.Compression" />
</ItemGroup>
</Project>
7 changes: 3 additions & 4 deletions XenCenterLib/Archive/ArchiveIterator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,16 +48,15 @@ public abstract class ArchiveIterator : IDisposable
public void ExtractAllContents(string pathToExtractTo, Action cancellingDelegate = null)
{
if (string.IsNullOrEmpty(pathToExtractTo))
throw new ArgumentNullException();
throw new ArgumentNullException(nameof(pathToExtractTo));

while (HasNext())
{
//make the path Windows friendly
var fileName = CurrentFileName();
var isDirectory = IsDirectory();

var sanitizedName = fileName.Replace('/', Path.DirectorySeparatorChar);
var conflatedPath = Path.Combine(pathToExtractTo, sanitizedName);
// Reject Zip Slip / path traversal before creating any filesystem paths.
var conflatedPath = ArchivePath.GetSafeExtractPath(pathToExtractTo, fileName);

var dir = isDirectory ? conflatedPath : Path.GetDirectoryName(conflatedPath);
dir = StringUtility.ToLongWindowsPath(dir, true);
Expand Down
Loading
Loading