Private/PathHelpers.ps1
|
function Test-IsAdmin { <# .SYNOPSIS Returns $true if running as Administrator, $false otherwise. #> [CmdletBinding()] param() return ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) } function Get-PathEntries { <# .SYNOPSIS Retrieves all PATH entries for both User and Machine scopes. #> [CmdletBinding()] param( [ValidateSet('User', 'Machine', 'Both')] [string]$Target = 'Both' ) $result = @{ User = @() Machine = @() } if ($Target -eq 'User' -or $Target -eq 'Both') { $userPath = [Environment]::GetEnvironmentVariable('PATH', 'User') if ($userPath) { $result.User = $userPath -split ';' | Where-Object { $_ -ne '' } } } if ($Target -eq 'Machine' -or $Target -eq 'Both') { $machinePath = [Environment]::GetEnvironmentVariable('PATH', 'Machine') if ($machinePath) { $result.Machine = $machinePath -split ';' | Where-Object { $_ -ne '' } } } return $result } function Test-PathEntry { <# .SYNOPSIS Tests if a PATH entry exists and is valid. #> [CmdletBinding()] param( [Parameter(Mandatory)] [string]$Path ) # Expand environment variables $expandedPath = [Environment]::ExpandEnvironmentVariables($Path) $exists = $false try { $exists = Test-Path -LiteralPath $expandedPath -PathType Container -ErrorAction Stop } catch { # Access denied or other errors - treat as inaccessible $exists = $false } return @{ Original = $Path Expanded = $expandedPath Exists = $exists } } function Find-DuplicatePaths { <# .SYNOPSIS Finds duplicate PATH entries (case-insensitive). #> [CmdletBinding()] param( [Parameter(Mandatory)] [string[]]$Paths ) $seen = @{} $duplicates = @() foreach ($path in $Paths) { $normalized = $path.TrimEnd('\').ToLowerInvariant() $expanded = [Environment]::ExpandEnvironmentVariables($normalized) if ($seen.ContainsKey($expanded)) { $duplicates += @{ Path = $path DuplicateOf = $seen[$expanded] } } else { $seen[$expanded] = $path } } return $duplicates } function Get-PathCharacterCount { <# .SYNOPSIS Gets the total character count of a PATH string. #> [CmdletBinding()] param( [string[]]$Paths ) if ($null -eq $Paths -or $Paths.Count -eq 0) { return 0 } return ($Paths -join ';').Length } function Test-PathSecurity { <# .SYNOPSIS Validates a path for security issues. .DESCRIPTION Checks for: - Forbidden characters in Windows paths (< > " | ? *) - Reserved Windows device names (CON, PRN, AUX, NUL, COM1-9, LPT1-9) - Path traversal attacks (..\ or ../) - Control characters (ASCII 0-31) - Null bytes (injection attacks) - Excessively long paths - UNC paths to potentially dangerous locations .PARAMETER Path The path to validate. .OUTPUTS Returns a hashtable with: - IsValid: $true if path is safe, $false otherwise - Issues: Array of security issues found - Severity: 'Safe', 'Warning', or 'Critical' .EXAMPLE Test-PathSecurity -Path "C:\Windows\System32" # Returns: @{ IsValid = $true; Issues = @(); Severity = 'Safe' } .EXAMPLE Test-PathSecurity -Path "C:\Users\..\Windows" # Returns: @{ IsValid = $false; Issues = @('Path traversal detected'); Severity = 'Critical' } #> [CmdletBinding()] [OutputType([hashtable])] param( [Parameter(Mandatory)] [AllowEmptyString()] [string]$Path ) $issues = @() $severity = 'Safe' # Check for empty or null path if ([string]::IsNullOrWhiteSpace($Path)) { return @{ IsValid = $false Issues = @('Path is empty or whitespace only') Severity = 'Critical' } } # Check for null bytes (injection attack vector) if ($Path.Contains([char]0)) { $issues += 'Null byte detected (potential injection attack)' $severity = 'Critical' } # Check for control characters (ASCII 0-31, except tab which is sometimes used) $controlChars = [regex]::Matches($Path, '[\x00-\x08\x0B\x0C\x0E-\x1F]') if ($controlChars.Count -gt 0) { $issues += "Control characters detected (ASCII codes: $($controlChars | ForEach-Object { [int][char]$_.Value } | Sort-Object -Unique))" $severity = 'Critical' } # Check for forbidden characters in Windows paths # Note: We allow : for drive letters (C:) and \ / for path separators # These characters are CRITICAL because they cannot exist in valid Windows paths $forbiddenChars = '<', '>', '"', '|', '?', '*' foreach ($char in $forbiddenChars) { if ($Path.Contains($char)) { $issues += "Forbidden character '$char' in path" $severity = 'Critical' } } # Check for reserved Windows device names # These cannot be used as file or folder names $reservedNames = @( 'CON', 'PRN', 'AUX', 'NUL', 'COM1', 'COM2', 'COM3', 'COM4', 'COM5', 'COM6', 'COM7', 'COM8', 'COM9', 'LPT1', 'LPT2', 'LPT3', 'LPT4', 'LPT5', 'LPT6', 'LPT7', 'LPT8', 'LPT9' ) # Split path and check each component $pathComponents = $Path -split '[\\\/]' | Where-Object { $_ -ne '' } foreach ($component in $pathComponents) { # Remove extension for comparison (CON.txt is also reserved) $nameWithoutExt = $component -replace '\.[^.]*$', '' if ($reservedNames -contains $nameWithoutExt.ToUpperInvariant()) { $issues += "Reserved Windows name '$component' in path" $severity = 'Critical' } } # Check for path traversal attacks # Look for patterns like ..\ or ../ that could escape intended directory if ($Path -match '\.\.[\\\/]' -or $Path -match '[\\\/]\.\.([\\\/]|$)') { $issues += 'Path traversal detected (..\ or ../ pattern)' $severity = 'Critical' } # Check for paths starting with .. (relative path traversal) if ($Path -match '^\.\.') { $issues += 'Path starts with parent directory reference (..)' if ($severity -ne 'Critical') { $severity = 'Warning' } } # Check for excessively long paths (Windows MAX_PATH is 260, but long paths can be 32767) if ($Path.Length -gt 32767) { $issues += "Path exceeds maximum length (32767 characters)" $severity = 'Critical' } elseif ($Path.Length -gt 260) { $issues += "Path exceeds legacy MAX_PATH (260 characters) - may not work on older systems" if ($severity -eq 'Safe') { $severity = 'Warning' } } # Check for suspicious UNC paths if ($Path -match '^\\\\') { # UNC path - add warning but don't block $issues += 'UNC network path detected - verify this is a trusted location' if ($severity -eq 'Safe') { $severity = 'Warning' } # Check for UNC paths to localhost with traversal if ($Path -match '^\\\\(localhost|127\.0\.0\.1|::1)\\.*\.\.') { $issues += 'Suspicious UNC path with traversal to localhost' $severity = 'Critical' } } # Check for paths with trailing dots or spaces (Windows normalizes these, can be confusing) if ($Path -match '\s+$' -or $Path -match '\.+$') { $issues += 'Path ends with spaces or dots (Windows will normalize these)' if ($severity -eq 'Safe') { $severity = 'Warning' } } # Check for multiple consecutive separators (could indicate path manipulation) if ($Path -match '[\\\/]{3,}') { $issues += 'Multiple consecutive path separators detected' if ($severity -eq 'Safe') { $severity = 'Warning' } } # Check for mixed path separators (potential confusion attack) if ($Path -match '\\' -and $Path -match '/') { $issues += 'Mixed path separators (\ and /) detected' if ($severity -eq 'Safe') { $severity = 'Warning' } } return @{ IsValid = ($severity -ne 'Critical') Issues = $issues Severity = $severity } } |