fix: escape dynamic path segments in three Remove-* cmdlets (#161)

* fix: escape dynamic path segments in three Remove-* cmdlets

Wrap Storage, Vnet, and Zone parameters with Uri.EscapeDataString() in
RemovePveStorageCmdlet, RemovePveSdnVnetCmdlet, and RemovePveSdnZoneCmdlet
to prevent path traversal attacks via the API path.

Add xUnit test demonstrating that escaped paths preserve percent-encoding
(preventing path collapse) while unescaped paths allow segment traversal.

Fixes #145

* fix: route Remove-Pve{Storage,SdnVnet,SdnZone} through their services

Delete the private PveHttpClient construction and inline
Uri.EscapeDataString call in RemovePveStorageCmdlet,
RemovePveSdnVnetCmdlet and RemovePveSdnZoneCmdlet; call
StorageService.RemoveStorage / NetworkService.RemoveSdnZone /
NetworkService.RemoveSdnVnet instead, which already escape the
identifier identically and are now the single place doing so.

Add ValidatePattern on the Storage/Vnet/Zone parameters as defense
in depth, anchored with \A/\z so a trailing newline cannot slip a
disallowed character past the gate.

Replace PveHttpClientPathEscapingTests with a version that actually
regression-tests the real Uri parser (asserts both that %2F survives
and that the unescaped form is absent), and add
StorageServiceTests/NetworkServiceTests cases that mock
IPveHttpClient and verify the exact escaped DELETE path for a
traversal-attempt name.

---------

Co-authored-by: goodolclint-claude[bot] <323206664+goodolclint-claude[bot]@users.noreply.github.com>
This commit is contained in:
goodolclint-claude[bot]
2026-09-02 16:59:57 +00:00
committed by GitHub
parent 68f953075d
commit 1bf7483a4e
6 changed files with 166 additions and 9 deletions
@@ -1,5 +1,5 @@
using System.Management.Automation;
using PSProxmoxVE.Core.Client;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Network
{
@@ -15,6 +15,7 @@ namespace PSProxmoxVE.Cmdlets.Network
{
/// <summary>The VNet identifier to remove.</summary>
[Parameter(Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true, HelpMessage = "The SDN VNet name.")]
[ValidatePattern(@"\A[A-Za-z0-9][A-Za-z0-9._-]*\z")]
public string Vnet { get; set; } = string.Empty;
protected override void ProcessRecord()
@@ -24,10 +25,10 @@ namespace PSProxmoxVE.Cmdlets.Network
var session = GetSession();
RequireVersion(session, "SDN", 6, 2, 8, 0);
using var client = new PveHttpClient(session);
var service = new NetworkService();
WriteVerbose($"Removing SDN VNet '{Vnet}'...");
client.DeleteAsync($"cluster/sdn/vnets/{Vnet}").GetAwaiter().GetResult();
service.RemoveSdnVnet(session, Vnet);
}
}
}
@@ -1,5 +1,5 @@
using System.Management.Automation;
using PSProxmoxVE.Core.Client;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Network
{
@@ -16,6 +16,7 @@ namespace PSProxmoxVE.Cmdlets.Network
{
/// <summary>The zone identifier to remove.</summary>
[Parameter(Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true, HelpMessage = "The SDN zone name.")]
[ValidatePattern(@"\A[A-Za-z0-9][A-Za-z0-9._-]*\z")]
public string Zone { get; set; } = string.Empty;
protected override void ProcessRecord()
@@ -25,10 +26,10 @@ namespace PSProxmoxVE.Cmdlets.Network
var session = GetSession();
RequireVersion(session, "SDN", 6, 2, 8, 0);
using var client = new PveHttpClient(session);
var service = new NetworkService();
WriteVerbose($"Removing SDN zone '{Zone}'...");
client.DeleteAsync($"cluster/sdn/zones/{Zone}").GetAwaiter().GetResult();
service.RemoveSdnZone(session, Zone);
}
}
}
@@ -1,5 +1,5 @@
using System.Management.Automation;
using PSProxmoxVE.Core.Client;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Storage
{
@@ -18,6 +18,7 @@ namespace PSProxmoxVE.Cmdlets.Storage
{
/// <summary>The storage identifier to remove.</summary>
[Parameter(Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true, HelpMessage = "The storage pool name.")]
[ValidatePattern(@"\A[A-Za-z0-9][A-Za-z0-9._-]*\z")]
public string Storage { get; set; } = string.Empty;
protected override void ProcessRecord()
@@ -26,10 +27,10 @@ namespace PSProxmoxVE.Cmdlets.Storage
return;
var session = GetSession();
using var client = new PveHttpClient(session);
var service = new StorageService();
WriteVerbose($"Removing storage '{Storage}'...");
client.DeleteAsync($"storage/{Storage}").GetAwaiter().GetResult();
service.RemoveStorage(session, Storage);
}
}
}