dotnet / runtime

.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.
https://docs.microsoft.com/dotnet/core/
MIT License
14.65k stars 4.57k forks source link

Binary formatter alternatives for resource serialisation #80155

Open ericcdub opened 1 year ago

ericcdub commented 1 year ago

BinaryFormatter is obsolete and soon to be removed from. NET due to security concerns regarding serialisation of arbitrary data. Coupled with this are changes in recent .NET releases to the way .resources files should be serialised to disk with ResourceWriter.

Resources of certain (non-primitive) types can no longer be serialised using ResourceWriter.AddResource as was possible in .NET Framework, and must be first serialised to byte arrays before being passed to PreserializedResourceWriter, but there is no suggestion as to what is the best method to serialise the resources into a byte array.

This is a problem because I'm serialising properties of form controls like System.Drawing.Size into .resources files as part of creating satellite assemblies. These types don't implement their own serialisation to my knowledge.

Therefore, I've been using BinaryFormatter to serialise objects to byte arrays before passing them PreserializedResourceWriter.AddBinaryaformattedResource.

If BinaryFormatter is now obsolete, how are developers supposed to serialise their resources?

dotnet-issue-labeler[bot] commented 1 year ago

I couldn't figure out the best area label to add to this issue. If you have write-permissions please help me learn by adding exactly one area label.

Clockwork-Muse commented 1 year ago

This is a problem because I'm serialising properties of form controls like System.Drawing.Size into .resources files as part of creating satellite assemblies.

For your specific case, assuming the properties are config equivalents (ie, width, text direction, etc), you should likely be using some form of text-based config or json file.

ericcdub commented 1 year ago

This is a problem because I'm serialising properties of form controls like System.Drawing.Size into .resources files as part of creating satellite assemblies.

For your specific case, assuming the properties are config equivalents (ie, width, text direction, etc), you should likely be using some form of text-based config or json file.

I don't think that'll work unfortunately as the .resources files being serialised will be used by Al.exe to create satellite assemblies for 3rd-party customer apps, so we don't get to pick and choose the format the resources are serialised with.

ghost commented 1 year ago

Tagging subscribers to this area: @dotnet/area-system-runtime See info in area-owners.md if you want to be subscribed.

Issue Details
BinaryFormatter is obsolete and soon to be removed from. NET due to security concerns regarding serialisation of arbitrary data. Coupled with this are changes in recent .NET releases to the way .resources files should be serialised to disk with ResourceWriter. Resources of certain (non-primitive) types can no longer be serialised using ResourceWriter.AddResource as was possible in .NET Framework, and must be first serialised to byte arrays before being passed to PreserializedResourceWriter, but there is no suggestion as to what is the best method to serialise the resources into a byte array. This is a problem because I'm serialising properties of form controls like System.Drawing.Size into .resources files as part of creating satellite assemblies. These types don't implement their own serialisation to my knowledge. Therefore, I've been using BinaryFormatter to serialise objects to byte arrays before passing them PreserializedResourceWriter.AddBinaryaformattedResource. If BinaryFormatter is now obsolete, how are developers supposed to serialise their resources?
Author: ericcdub
Assignees: -
Labels: `area-Serialization`, `area-System.Runtime`, `untriaged`
Milestone: -
dakersnar commented 1 year ago

cc @GrabYourPitchforks

sengiv commented 1 year ago

It's high time to make your own BinaryFormatter

tannergooding commented 11 months ago

@GrabYourPitchforks, could you take a look at this as it seems related to binary serialization?

Neme12 commented 11 months ago

properties of form controls like System.Drawing.Size

System.Drawing.Size should have a TypeConverter, so you should be able to do:

TypeDescriptor.GetConverter(typeof(Size)).ConvertToInvariantString(size);

and

TypeDescriptor.GetConverter(typeof(Size)).ConvertFromInvariantString(size);

Many .NET types have a TypeConverter, so it might be enough for your needs.

GrabYourPitchforks commented 11 months ago

The only way to do this safely is to have the resource reader and writer special-case specific types or data structures and to write them out using whatever format is appropriate for them.

For example, if you're trying to serialize a System.Drawing.Bitmap, you could write out a marker saying "this is an image" and then follow it up with the raw image data. On deserialization, the resource reader would read the "this is an image" marker and say ok - I need to take this byte[] and call the Bitmap ctor over it.

If you're trying to serialize a Rect or a Size or a similar type, the writer would emit the appropriate marker followed by the data, and when the reader reads the marker, it goes down whatever specialized logic is needed to read the resource.

Here's the crucial detail. If you want this to not have the same problems BinaryFormatter and related serializers have, you must follow one of the below patterns.

  1. The resource reader / writer needs to special-case all the types it cares about. This path means the set of resource-enabled types is finite, and you cannot add your own custom types to this list.

  2. Or you need to invent a brand new interface for resource reading / writing extensibility, and custom types would need to implement this interface. (TypeConverter isn't really meant for structured data.) Crucially, since the resource reader would be forbidden from calling Type.GetType or Assembly.GetType, this would require some agent external to the .resx file itself to provide the full closure of resource-enabled serializable types.

This is all a lot of work, when a much simpler solution exists today:

Use string-based resources. If you need to smuggle custom types, then use JSON or XML or whatever mechanism is appropriate to turn your custom data object into a string. Then when you read the resource, read it back as a string, then JSON/XML/whatever-deserialize it back into the data structure you expect.

ericcdub commented 11 months ago

The only way to do this safely is to have the resource reader and writer special-case specific types or data structures and to write them out using whatever format is appropriate for them.

For example, if you're trying to serialize a System.Drawing.Bitmap, you could write out a marker saying "this is an image" and then follow it up with the raw image data. On deserialization, the resource reader would read the "this is an image" marker and say ok - I need to take this byte[] and call the Bitmap ctor over it.

If you're trying to serialize a Rect or a Size or a similar type, the writer would emit the appropriate marker followed by the data, and when the reader reads the marker, it goes down whatever specialized logic is needed to read the resource.

Here's the crucial detail. If you want this to not have the same problems BinaryFormatter and related serializers have, you must follow one of the below patterns.

  1. The resource reader / writer needs to special-case all the types it cares about. This path means the set of resource-enabled types is finite, and you cannot add your own custom types to this list.
  2. Or you need to invent a brand new interface for resource reading / writing extensibility, and custom types would need to implement this interface. (TypeConverter isn't really meant for structured data.) Crucially, since the resource reader would be forbidden from calling Type.GetType or Assembly.GetType, this would require some agent external to the .resx file itself to provide the full closure of resource-enabled serializable types.

This is all a lot of work, when a much simpler solution exists today:

Use string-based resources. If you need to smuggle custom types, then use JSON or XML or whatever mechanism is appropriate to turn your custom data object into a string. Then when you read the resource, read it back as a string, then JSON/XML/whatever-deserialize it back into the data structure you expect.

Thanks for your detailed reply. The key point here is that our use case is creating satellite assemblies for third party applications supplied by customers, not our own applications. As such, we're not in a position to impose our own custom resource serialization protocols on third party applications. The customer would have to modify their app to perform this custom serialization in their own code, wouldn't they? The .NET runtime presumably expects resources in satellite assemblies to adhere to a proprietary Microsoft serialization format so a custom format will never work out of the box - I believe it's called MS-NRBF?

I'm wondering if it's better for us to write a .ResX file instead of a .resources file, and pass that ResX to Al.exe?

ericcdub commented 7 months ago

Just commenting here to see if there's been any update regarding BinaryFormatter and what to replace it with for WinForms resources?

mastahg commented 6 months ago

I'm coming across this issue in porting my .net framework application to .net8. Our software allows the compilation of C# during runtime via Microsoft.CodeAnalysis.CSharp.CSharpCompilation

The following works on .net framework, but will throw an error when you feed it a .resx created by visual studio that contains icons,sizes,points etc on .net8

private static Stream ProvideResourceData(string resourceFullFilename)
        {
            // For non-.resx files just create a FileStream object to read the file as binary data
            if (!resourceFullFilename.EndsWith(".resx", StringComparison.OrdinalIgnoreCase))
                return new FileStream(resourceFullFilename, FileMode.Open);

            // Remainder of this method converts a .resx file into .resource file data and returns it 
            //  as a MemoryStream
            MemoryStream shortLivedBackingStream = new MemoryStream();
            using (ResourceWriter resourceWriter = new ResourceWriter(shortLivedBackingStream))
            {
                //resourceWriter.TypeNameConverter = TypeNameConverter;
                using (ResXResourceReader resourceReader = new ResXResourceReader(resourceFullFilename))
                {

                    resourceReader.BasePath = Path.GetDirectoryName(Path.GetFullPath(resourceFullFilename));
                                        foreach (DictionaryEntry dictionaryEnumerator in resourceReader)
                    {

                        string resourceKey = dictionaryEnumerator.Key as string;
                            resourceWriter.AddResource(resourceKey, dictionaryEnumerator.Value);

                }
            }

            // This needed because shortLivedBackingStream is now closed
            return new MemoryStream(shortLivedBackingStream.GetBuffer());
        }

Using ericcdub's workaround

                                using var tempMS = new MemoryStream();
                                bf.Serialize(tempMS,dictionaryEnumerator.Value);
                                tempMS.Position = 0;
                                resourceWriter.AddBinaryFormattedResource(resourceKey, tempMS.ToArray());