Functions/GenXdev.Helpers/SecurityHelpers.cs
// ################################################################################
// Part of PowerShell module : GenXdev.Helpers // Original cmdlet filename : SecurityHelpers.cs // Original author : René Vaessen / GenXdev // Version : 1.282.2025 // ################################################################################ // MIT License // // Copyright 2021-2025 GenXdev // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // ################################################################################ using System.Net; using System.Text; namespace GenXdev.Helpers { public static class SecurityHelper { /// <summary> /// Sanitizes a filename by replacing invalid characters with underscores and optionally adding a unique suffix. /// </summary> /// <param name="name">The filename to sanitize.</param> /// <param name="giveUniqueSuffix">Whether to append a unique hash-based suffix to prevent collisions.</param> /// <returns>Sanitized filename safe for filesystem operations.</returns> public static string SanitizeFileName(string name, bool giveUniqueSuffix = false) { // get system-defined invalid path characters var invalidChars = Path.GetInvalidPathChars(); // build sanitized string character by character var sb = new StringBuilder(name.Length); foreach (var c in name.ToCharArray()) { // replace invalid characters with underscore if (Array.IndexOf(invalidChars, c) >= 0) { sb.Append('_'); } else // allow only safe characters for filenames switch (Char.ToLower(c)) { case 'a': case 'b': case 'c': case 'd': case 'e': case 'f': case 'g': case 'h': case 'i': case 'j': case 'k': case 'l': case 'm': case 'n': case 'o': case 'p': case 'q': case 'r': case 's': case 't': case 'u': case 'v': case 'w': case 'x': case 'y': case 'z': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': case '0': case '!': case '@': case '#': case '$': case '&': case '(': case ')': case '_': case '-': case '+': case '=': case '{': case '}': case '[': case ']': case '.': case ',': sb.Append(c); break; default: // replace unsafe characters with underscore sb.Append('_'); break; } } // append unique suffix based on hash if requested if (giveUniqueSuffix) { sb.Append("_" + ((UInt32)name.Trim().ToLowerInvariant().GetHashCode()).ToString().PadLeft(10, '0')); } // clean up double underscores return sb.ToString().Replace("__", "_"); } /// <summary> /// Determines if a URL string points to a safe public host by checking if all resolved IP addresses are public. /// </summary> /// <param name="URL">The URL string to validate.</param> /// <returns>True if the URL resolves to public IP addresses only.</returns> public static bool IsSafePublicURL(string URL) { try { // parse URL and check host Uri url = new Uri(URL); return HostOrIPPublic(url.Host); } catch { // invalid URL format is not safe return false; } } /// <summary> /// Determines if a Uri object points to a safe public host by checking if all resolved IP addresses are public. /// </summary> /// <param name="URL">The Uri object to validate.</param> /// <returns>True if the Uri resolves to public IP addresses only.</returns> public static bool IsSafePublicURL(Uri URL) { // delegate to host checking logic return HostOrIPPublic(URL.Host); } /// <summary> /// Checks if a hostname resolves to public IP addresses only (not private or local). /// </summary> /// <param name="HostName">The hostname to check.</param> /// <returns>True if all resolved addresses are public.</returns> public static bool HostOrIPPublic(string HostName) { // assume public until proven otherwise bool result = true; try { // resolve hostname to IP addresses IPAddress[] addresslist = Dns.GetHostAddresses(HostName.Trim()); // check each resolved address foreach (IPAddress address in addresslist) { if (!IsPublicIpAddress(address)) { // any private address makes the host not safe return false; } } } catch { // DNS resolution failure means not safe result = false; } return result; } /// <summary> /// Asynchronously checks if a hostname resolves to public IP addresses only (not private or local). /// </summary> /// <param name="HostName">The hostname to check.</param> /// <returns>True if all resolved addresses are public.</returns> public static async Task<bool> HostOrIPPublicAsync(string HostName) { // assume public until proven otherwise bool result = true; try { // asynchronously resolve hostname to IP addresses var addresslist = await Task.Factory.FromAsync<IPAddress[]>( Dns.BeginGetHostAddresses(HostName.Trim(), null, null), Dns.EndGetHostAddresses); // check each resolved address foreach (IPAddress address in addresslist) { if (!IsPublicIpAddress(address)) { // any private address makes the host not safe return false; } } } catch { // DNS resolution failure means not safe result = false; } return result; } /// <summary> /// Determines if an IP address is public (not private, local, or reserved). /// </summary> /// <param name="address">The IP address to check.</param> /// <returns>True if the address is public and safe for external connections.</returns> public static bool IsPublicIpAddress(IPAddress address) { try { // parse IPv4 address into octets String[] straryIPAddress = address.ToString().Split(new String[] { "." }, StringSplitOptions.RemoveEmptyEntries); int[] iaryIPAddress = new int[] { int.Parse(straryIPAddress[0]), int.Parse(straryIPAddress[1]), int.Parse(straryIPAddress[2]), int.Parse(straryIPAddress[3]) }; // check for private IPv4 ranges if (iaryIPAddress[0] == 10 || // 10.0.0.0/8 (iaryIPAddress[0] == 127 && (iaryIPAddress[1] == 0) && (iaryIPAddress[2] == 0) && (iaryIPAddress[3] == 1)) || // localhost (iaryIPAddress[0] == 192 && iaryIPAddress[1] == 168) || // 192.168.0.0/16 (iaryIPAddress[0] == 172 && (iaryIPAddress[1] >= 16 && iaryIPAddress[1] <= 31)) // 172.16.0.0/12 ) { return false; } } catch { // parsing error means not safe } return true; } } } |