Private/PathSafety.ps1

# Helpers for safely turning caller-supplied names into paths.
#
# Commands that accept a file or template *name* and join it onto an internal
# directory (attachments, documents, recurring templates) must ensure the name
# is a plain leaf. Otherwise a value such as '..\..\journal.txt' would let a
# delete or write escape the intended directory (path traversal).

function Assert-LedgerSafeLeafName {
    <#
    .SYNOPSIS
    Validates that a caller-supplied name is a plain file/leaf name and returns it.

    .DESCRIPTION
    Throws if the name is empty, contains a path separator, is drive- or
    root-qualified, or is a relative-directory reference ('.' or '..'). On
    success the original name is returned so callers can write
    "$Name = Assert-LedgerSafeLeafName -Name $Name".
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [AllowNull()]
        [AllowEmptyString()]
        [string]$Name,

        # Noun used in the error message (e.g. 'attachment', 'document').
        [Parameter()]
        [string]$Kind = 'file'
    )

    if ([string]::IsNullOrWhiteSpace($Name)) {
        throw "Invalid $Kind name: the name must not be empty."
    }

    if ($Name.IndexOfAny([char[]]('/', '\')) -ge 0 -or
        [System.IO.Path]::IsPathRooted($Name) -or
        $Name -eq '.' -or $Name -eq '..' -or
        [System.IO.Path]::GetFileName($Name) -ne $Name) {
        throw "Invalid $Kind name '$Name': only a plain file name is allowed (no path separators, drive qualifiers or '..')."
    }

    return $Name
}