diff --git a/PSFramework/PSFramework.psd1 b/PSFramework/PSFramework.psd1 index e2417384..a7554df0 100644 --- a/PSFramework/PSFramework.psd1 +++ b/PSFramework/PSFramework.psd1 @@ -4,7 +4,7 @@ RootModule = 'PSFramework.psm1' # Version number of this module. - ModuleVersion = '1.11.343' + ModuleVersion = '1.12.345' # ID used to uniquely identify this module GUID = '8028b914-132b-431f-baa9-94a6952f21ff' diff --git a/PSFramework/bin/PSFramework.dll b/PSFramework/bin/PSFramework.dll index e3484727..df69ae40 100644 Binary files a/PSFramework/bin/PSFramework.dll and b/PSFramework/bin/PSFramework.dll differ diff --git a/PSFramework/bin/PSFramework.pdb b/PSFramework/bin/PSFramework.pdb index 5b2617dc..fd07ac09 100644 Binary files a/PSFramework/bin/PSFramework.pdb and b/PSFramework/bin/PSFramework.pdb differ diff --git a/PSFramework/bin/PSFramework.xml b/PSFramework/bin/PSFramework.xml index 73d9f8f7..57b50953 100644 --- a/PSFramework/bin/PSFramework.xml +++ b/PSFramework/bin/PSFramework.xml @@ -7546,6 +7546,20 @@ Allows remapping assembly-names for objects being deserialized, using an abbreviated name only, to help avoid having to be version specific. + + + Compress string using default zip algorithms + + The string to compress + Returns a compressed string as byte-array. + + + + Expand a string using default zig algorithms + + The compressed string to expand + Returns an expanded string. + Compares two completion results diff --git a/PSFramework/changelog.md b/PSFramework/changelog.md index f221e33f..2e0082b6 100644 --- a/PSFramework/changelog.md +++ b/PSFramework/changelog.md @@ -1,5 +1,25 @@ # CHANGELOG +## 1.12.345 (2024-09-17) + +> Breaking Change + +SerializationTypeConverter changed from using BinaryFormatter to using DataContractSerializer instead, avoiding a critical security vulnerability. This change will _not_ affect anybody not using this component to prevent Deserialized objects when sending objects from formal classes from one PowerShell process to another (e.g. with remoting). Regular PowerShell execution - including remoting - remains unaffected (only without the vulnerability). + +Actual impact on modules implementing this component: + +- "Failure" always means a fallback to "Deserialized." objects, not actual exceptions. +- The new version must be deployed on both ends of the communication, otherwise implemented deserialization will fail. +- The new version will fail to import clixml files exported with the old version +- All sub-properties must adhere to the serialization rules, not just the top level class. Previously it was possible to have your own class have an "object"-typed property and only the content of that property would be a "deserialized." object, rather the entire item. This no longer works. + +This critical security vulnerability superseded the reliability promise, but should fortunately have little impact on almost all existing use of the module. + +> Change List + +- Sec: Critical security update to the `SerializationTypeConverter` class and PS Object Serialization extension component. +- Fix: ConvertTo-PSFHashtable - `-Remap` fails when trying to fix the casing on a key. (#641) + ## 1.11.343 (2024-07-18) - Fix: Disable-PSFLoggingProvider - fails with timeout error. diff --git a/library/PSFramework/Commands/ConvertToPSFHashtableCommand.cs b/library/PSFramework/Commands/ConvertToPSFHashtableCommand.cs index d8677dbc..9188d763 100644 --- a/library/PSFramework/Commands/ConvertToPSFHashtableCommand.cs +++ b/library/PSFramework/Commands/ConvertToPSFHashtableCommand.cs @@ -182,8 +182,9 @@ protected override void ProcessRecord() { if (result.ContainsKey(key)) { - result[Remap[key]] = result[key]; + object value = result[key]; result.Remove(key); + result[Remap[key]] = value; } } } diff --git a/library/PSFramework/PSFramework.csproj b/library/PSFramework/PSFramework.csproj index 1346d4cb..e57a127a 100644 --- a/library/PSFramework/PSFramework.csproj +++ b/library/PSFramework/PSFramework.csproj @@ -53,6 +53,7 @@ C:\Windows\Microsoft.NET\assembly\GAC_MSIL\System.Management.Automation\v4.0_3.0.0.0__31bf3856ad364e35\System.Management.Automation.dll False + diff --git a/library/PSFramework/Serialization/SerializationTypeConverter.cs b/library/PSFramework/Serialization/SerializationTypeConverter.cs index 3de488ce..9880678b 100644 --- a/library/PSFramework/Serialization/SerializationTypeConverter.cs +++ b/library/PSFramework/Serialization/SerializationTypeConverter.cs @@ -1,11 +1,14 @@ using System; using System.Collections.Concurrent; using System.IO; +using System.IO.Compression; using System.Management.Automation; using System.Reflection; using System.Runtime.Serialization; -using System.Runtime.Serialization.Formatters.Binary; +using System.Text; using System.Text.RegularExpressions; +using System.Xml; +using System.Xml.Linq; namespace PSFramework.Serialization { @@ -14,7 +17,7 @@ namespace PSFramework.Serialization /// public class SerializationTypeConverter : PSTypeConverter { - private static ResolveEventHandler AssemblyHandler = new ResolveEventHandler(SerializationTypeConverter.CurrentDomain_AssemblyResolve); + private static ResolveEventHandler AssemblyHandler = new ResolveEventHandler(CurrentDomain_AssemblyResolve); /// /// Whether the source can be converted to its destination @@ -87,7 +90,7 @@ private bool CanConvert(object sourceValue, Type destinationType, out byte[] ser error = new NotSupportedException(string.Format("Unsupported Source Type: {0}", sourceValue.GetType().FullName)); return false; } - if (!SerializationTypeConverter.CanSerialize(destinationType)) + if (!CanSerialize(destinationType)) { error = new NotSupportedException(string.Format("Unsupported Type Conversion: {0}", destinationType.FullName)); return false; @@ -125,24 +128,19 @@ private object DeserializeObject(object sourceValue, Type destinationType) throw ex; } object obj; - using (MemoryStream memoryStream = new MemoryStream(buffer)) + + AppDomain.CurrentDomain.AssemblyResolve += AssemblyHandler; + try { - AppDomain.CurrentDomain.AssemblyResolve += SerializationTypeConverter.AssemblyHandler; - try - { - BinaryFormatter binaryFormatter = new BinaryFormatter(); - obj = binaryFormatter.Deserialize(memoryStream); - PSFCore.PSFCoreHost.WriteDebug("Serializer.DeserializeObject.Obj", obj); - IDeserializationCallback deserializationCallback = obj as IDeserializationCallback; - if (deserializationCallback != null) - { - deserializationCallback.OnDeserialization(sourceValue); - } - } - finally - { - AppDomain.CurrentDomain.AssemblyResolve -= SerializationTypeConverter.AssemblyHandler; - } + obj = ConvertFromXml(ExpandString(buffer), destinationType); + PSFCore.PSFCoreHost.WriteDebug("Serializer.DeserializeObject.Obj", obj); + IDeserializationCallback deserializationCallback = obj as IDeserializationCallback; + if (deserializationCallback != null) + deserializationCallback.OnDeserialization(sourceValue); + } + finally + { + AppDomain.CurrentDomain.AssemblyResolve -= AssemblyHandler; } return obj; } @@ -152,13 +150,13 @@ private object DeserializeObject(object sourceValue, Type destinationType) /// public static void RegisterAssemblyResolver() { - AppDomain.CurrentDomain.AssemblyResolve += SerializationTypeConverter.AssemblyHandler; + AppDomain.CurrentDomain.AssemblyResolve += AssemblyHandler; } private static Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args) { PSFCore.PSFCoreHost.WriteDebug("Serializer.AssemblyResolve.Sender", sender); PSFCore.PSFCoreHost.WriteDebug("Serializer.AssemblyResolve.Args", args); - + // 1) Match directly against existing assembly Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies(); for (int i = 0; i < assemblies.Length; i++) @@ -169,7 +167,7 @@ private static Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEven string shortName = args.Name.Split(',')[0]; if (AssemblyShortnameMapping.Count > 0 && AssemblyShortnameMapping[shortName]) for (int i = 0; i < assemblies.Length; i++) - if (String.Equals(assemblies[i].FullName.Split(',')[0],shortName, StringComparison.InvariantCultureIgnoreCase)) + if (String.Equals(assemblies[i].FullName.Split(',')[0], shortName, StringComparison.InvariantCultureIgnoreCase)) return assemblies[i]; if (AssemblyMapping.Count == 0) @@ -201,7 +199,7 @@ private static Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEven /// Whether the object can be serialized public static bool CanSerialize(object obj) { - return obj != null && SerializationTypeConverter.CanSerialize(obj.GetType()); + return obj != null && CanSerialize(obj.GetType()); } /// @@ -211,7 +209,7 @@ public static bool CanSerialize(object obj) /// Whether the specified type can be serialized public static bool CanSerialize(Type type) { - return SerializationTypeConverter.TypeIsSerializable(type) && !type.IsEnum || (type.Equals(typeof(Exception)) || type.IsSubclassOf(typeof(Exception))); + return TypeIsSerializable(type) && !type.IsEnum || (type.Equals(typeof(Exception)) || type.IsSubclassOf(typeof(Exception))); } /// @@ -237,7 +235,7 @@ public static bool TypeIsSerializable(Type type) for (int i = 0; i < genericArguments.Length; i++) { Type type2 = genericArguments[i]; - if (!SerializationTypeConverter.TypeIsSerializable(type2)) + if (!TypeIsSerializable(type2)) { return false; } @@ -252,16 +250,9 @@ public static bool TypeIsSerializable(Type type) /// A memory stream. public static object GetSerializationData(PSObject psObject) { - object result; - using (MemoryStream memoryStream = new MemoryStream()) - { - BinaryFormatter binaryFormatter = new BinaryFormatter(); - binaryFormatter.Serialize(memoryStream, psObject.BaseObject); - result = memoryStream.ToArray(); - } - return result; + return CompressString(ConvertToXml(psObject.BaseObject)); } - + /// /// Allows remapping assembly-names for objects being deserialized, using the full assembly-name. /// @@ -270,5 +261,71 @@ public static object GetSerializationData(PSObject psObject) /// Allows remapping assembly-names for objects being deserialized, using an abbreviated name only, to help avoid having to be version specific. /// public static readonly ConcurrentDictionary AssemblyShortnameMapping = new ConcurrentDictionary(StringComparer.InvariantCultureIgnoreCase); + + public static string ConvertToXml(object Item) + { + if (Item == null) + throw new ArgumentNullException("item"); + + string result; + using (StringWriter writer = new StringWriter()) + using (XmlTextWriter xmlWriter = new XmlTextWriter(writer)) + { + DataContractSerializer serializer = new DataContractSerializer(Item.GetType()); + serializer.WriteObject(xmlWriter, Item); + result = writer.ToString(); + } + return result; + } + + public static object ConvertFromXml(string Xml, Type ExpectedType) + { + object result; + using (StringReader reader = new StringReader(Xml)) + using (XmlTextReader xmlReader = new XmlTextReader(reader)) + { + DataContractSerializer serializer = new DataContractSerializer(ExpectedType); + result = serializer.ReadObject(xmlReader); + } + return result; + } + + /// + /// Compress string using default zip algorithms + /// + /// The string to compress + /// Returns a compressed string as byte-array. + public static byte[] CompressString(string String) + { + byte[] bytes = Encoding.UTF8.GetBytes(String); + using (MemoryStream outputStream = new MemoryStream()) + using (GZipStream gZipStream = new GZipStream(outputStream, CompressionMode.Compress)) + { + gZipStream.Write(bytes, 0, bytes.Length); + gZipStream.Close(); + outputStream.Close(); + return outputStream.ToArray(); + } + } + + /// + /// Expand a string using default zig algorithms + /// + /// The compressed string to expand + /// Returns an expanded string. + public static string ExpandString(byte[] CompressedString) + { + using (MemoryStream inputStream = new MemoryStream(CompressedString)) + using (MemoryStream outputStream = new MemoryStream()) + using (GZipStream converter = new GZipStream(inputStream, CompressionMode.Decompress)) + { + converter.CopyTo(outputStream); + converter.Close(); + inputStream.Close(); + string result = Encoding.UTF8.GetString(outputStream.ToArray()); + outputStream.Close(); + return result; + } + } } }