Repository: MessagePack-CSharp/MessagePack-CSharp

Issue:
TypeAccessException error from non-hash-safe key used in UntrustedData mode
Thank you for your hard work.

After checking the fix in #2034, deserialization failed.

```
TypeAccessException: No hash-resistant equality comparer available for type: Localization.Runtime.LocalizeKey
MessagePack.MessagePackSecurity.GetHashCollisionResistantEqualityComparer[T] () (at <5f2070410d8a408cacd68ee6662e8e3f>:0)
MessagePack.MessagePackSecurity.GetEqualityComparer[T] () (at <5f2070410d8a408cacd68ee6662e8e3f>:0)
MessagePack.Formatters.DictionaryFormatter`2[TKey,TValue].Create (System.Int32 count, MessagePack.MessagePackSerializerOptions options) (at <5f2070410d8a408cacd68ee6662e8e3f>:0)
MessagePack.Formatters.DictionaryFormatterBase`5[TKey,TValue,TIntermediate,TEnumerator,TDictionary].Deserialize (MessagePack.MessagePackReader& reader, MessagePack.MessagePackSerializerOptions options) (at <5f2070410d8a408cacd68ee6662e8e3f>:0)
MessagePack.GeneratedMessagePackResolver+Localization+Runtime+LocalizeStringsFormatter.Deserialize (MessagePack.MessagePackReader& reader, MessagePack.MessagePackSerializerOptions options) (at MessagePack.SourceGenerator/MessagePack.SourceGenerator.MessagePackGenerator/Formatters.MessagePack.GeneratedMessagePackResolver.Localization.Runtime.LocalizeStringsFormatter.g.cs:48)
MessagePack.MessagePackSerializer.Deserialize[T] (MessagePack.MessagePackReader& reader, MessagePack.MessagePackSerializerOptions options) (at <5f2070410d8a408cacd68ee6662e8e3f>:0)
Rethrow as MessagePackSerializationException: Failed to deserialize Localization.Runtime.LocalizeStrings value.
MessagePack.MessagePackSerializer.Deserialize[T] (MessagePack.MessagePackReader& reader, MessagePack.MessagePackSerializerOptions options) (at <5f2070410d8a408cacd68ee6662e8e3f>:0)
MessagePack.MessagePackSerializer.DeserializeFromSequenceAndRewindStreamIfPossible[T] (System.IO.Stream streamToRewind, MessagePack.MessagePackSerializerOptions options, System.Buffers.ReadOnlySequence`1[T] sequence, System.Threading.CancellationToken cancellationToken) (at <5f2070410d8a408cacd68ee6662e8e3f>:0)
MessagePack.MessagePackSerializer.Deserialize[T] (System.IO.Stream stream, MessagePack.MessagePackSerializerOptions options, System.Threading.CancellationToken cancellationToken) (at <5f2070410d8a408cacd68ee6662e8e3f>:0)
Localization.Runtime.Localizables.LoadText (System.String baseFileName) (at Assets/Plugins/Localization/Runtime/Localizables.cs:418)
UnityEngine.Debug:LogException(Exception)
Localization.Runtime.Localizables:LoadText(String) (at Assets/Plugins/Localization/Runtime/Localizables.cs:424)
Localization.Runtime.Localizables:SetLocaleCore(String) (at Assets/Plugins/Localization/Runtime/Localizables.cs:255)
Localization.Runtime.Localizables:SetLocale(String) (at Assets/Plugins/Localization/Runtime/Localizables.cs:228)
Localization.Runtime.Localizables:Initialize() (at Assets/Plugins/Localization/Runtime/Localizables.cs:181)
Localization.Runtime.Localizables:EnsureInitialized() (at Assets/Plugins/Localization/Runtime/Localizables.cs:156)
Localization.Runtime.Localizables:.cctor() (at Assets/Plugins/Localization/Runtime/Localizables.cs:110)
CoreSystem.Runtime.Shared.UISystem.Components.CustomTMPText:ApplyLocalizeFontSize() (at Assets/Plugins/CoreSystem/Runtime/Shared/UISystem/Components/CustomTMPText.cs:222)
UISystem.Runtime.Components.CustomTMPText:Awake() (at Assets/Plugins/UISystem/Runtime/Components/CustomTMPText.cs:239)

```

```
using System;
using System.Diagnostics;
using TextControlSystem.Runtime;
using MessagePack;
using MessagePack.Formatters;

namespace Localization.Runtime
{
	[MessagePackObject]
	[MessagePackFormatter(typeof(Formatter))]
	[DebuggerDisplay("\"{ToString()}\"")]
	public readonly struct LocalizeKey : IEquatable<LocalizeKey>
	{
		private const char SEPARATOR = '.';

		public string RawText { get; }

		private int Offset { get; }

		public int Length { get; }

		public bool HasCategory => RawText.IndexOf(SEPARATOR, Offset) != -1;

		public static implicit operator LocalizeKey(string id) => new (id);

		public static implicit operator StringSlice(LocalizeKey key)
			=> new StringSlice(key.RawText, key.Offset, key.Length);

		public LocalizeKey(string id)
		{
			RawText = id;
			Offset = 0;
			Length = RawText.Length;
		}

		private LocalizeKey(LocalizeKey id, int startIndex)
		{
			RawText = id.RawText;
			Offset = Math.Min(startIndex, RawText.Length);

			var endIndex = RawText.IndexOf(SEPARATOR, startIndex);
			if (endIndex == -1)
			{
				endIndex = RawText.Length;
			}
			Length = endIndex - Offset;
		}

		public LocalizeKey GetTopCategoryId()
		{
			return new LocalizeKey(this, Offset);
		}

		public LocalizeKey GetSubTextId()
		{
			var nextStartIndex = RawText.IndexOf(SEPARATOR, Offset);
			return new LocalizeKey(this, Math.Min(nextStartIndex + 1, RawText.Length));
		}

		public override string ToString()
		{
			return RawText.Substring(Offset, Length);
		}

		public override int GetHashCode()
		{
			if (RawText == null)
				return 0;

			var hashCode = typeof(LocalizeKey).GetHashCode();
			for (var i = 0; i < Length; i++)
			{
				hashCode ^= RawText[i + Offset];
			}

			return hashCode;
		}

		public bool Equals(LocalizeKey other)
		{
			if (RawText == other.RawText &&
			    Offset == other.Offset &&
			    Length == other.Length)
			{
				return true;
			}

			if (Length != other.Length)
			{
				return false;
			}

			for (var i = 0; i < Length; i++)
			{
				if (RawText[i + Offset] != other.RawText[i + other.Offset])
				{
					return false;
				}
			}

			return true;
		}

		public override bool Equals(object obj)
		{
			switch (obj)
			{
				case LocalizeKey other when Equals(other):
				case string str when Equals(new LocalizeKey(str)):
					return true;
				default:
					return false;
			}
		}

		internal class Formatter : IMessagePackFormatter<LocalizeKey>
		{
			public void Serialize(ref MessagePackWriter writer, LocalizeKey value, MessagePackSerializerOptions options)
			{
				// ToolOnly
				throw new NotImplementedException();
			}

			public LocalizeKey Deserialize(ref MessagePackReader reader, MessagePackSerializerOptions options)
			{
				if (reader.TryReadNil())
				{
					return default;
				}

				var formatterResolver = options.Resolver;
				var str = formatterResolver.GetFormatterWithVerify<string>().Deserialize(ref reader, options);

				return new LocalizeKey(str);
			}
		}

		public static string GetLabel(string key)
		{
			var index = key.LastIndexOf('.');
			return index >= 0 ? key[(index + 1)..] : key;
		}
	}
}

```

When I serialized something with v3.0.238-rc.1
and deserialized it with v3.0.214-rc.1, it was successful, so I think this is due to the fixes in v3.0.214-rc.1 to v3.0.238-rc.1.

Unity 6000.0.10f1
Message Pack v3.0.238-rc.1


Hints:
This is by design because v3 now runs in UntrustedData mode by default. Learn more [in our readme file](https://github.com/MessagePack-CSharp/MessagePack-CSharp/tree/develop?tab=readme-ov-file#security). You can resolve this either by switching back to TrustedData or providing your own secure hash function as documented.
Sorry

It definitely worked after I changed it to TrustedData

Thank you for the explanation
@AArnott 
Ah, I hadn't thought it through deeply enough.
I thought UntrustedData would be fine as default because I didn't think these issues would occur.
This is too big of a breaking change.
Dictionaries with custom keys are quite common.
Having it throw exceptions by default is too strict behavior-wise.
Is there a way we can make it secure by default while preventing exceptions?

```csharp
// minimum example of throw exception

var options = MessagePackSerializerOptions.Standard.WithSecurity(MessagePackSecurity.UntrustedData);

var dict = new Dictionary<CustomKey, int> { { new CustomKey(0), 0 } };
var bin = MessagePackSerializer.Serialize(dict, options);
var dict2 = MessagePackSerializer.Deserialize<Dictionary<CustomKey, int>>(bin, options); // throws

[MessagePackObject]
public struct CustomKey(int x) : IEquatable<CustomKey>
{
    [Key(0)]
    public int X { get; } = x;

    public bool Equals(CustomKey other)
    {
        return this.X == other.X;
    }

    public override int GetHashCode()
    {
        return X;
    }
}
```
Since a custom GetHashCode is part of the logic, honestly, I feel like it might be fine to just pass it through as-is...
> This is too big of a breaking change.

Bummer. I guess I'll revert the change.

> Is there a way we can make it secure by default while preventing exceptions?

This is challenging. Only one idea occurs to me on how we could apply a secure hash to everything automatically. I happen to be exploring it [here](https://github.com/eiriktsarpalis/typeshape-csharp/issues/33#issuecomment-2441413241), which you're welcome to chime in on.
This is slightly off topic, but if TrustedData has to be the default in the end, it is a good to set MaximumObjectGraphDepth to 500 on TrustedData.

Patch:
diff --git a/README.md b/README.md
index 2751b4ba1..fdc288e84 100644
--- a/README.md
+++ b/README.md
@@ -665,16 +665,11 @@ Untrusted data might come from over the network from an untrusted source (e.g. a
 
 Please be very mindful of these attack scenarios; many projects and companies, and serialization library users in general, have been bitten by untrusted user data deserialization in the past.
 
-You should also avoid the Typeless serializer/formatters/resolvers for untrusted data as that opens the door for the untrusted data to potentially deserialize unanticipated types that can compromise security.
-
-MessagePack v3 assumes the data you deserialize is untrusted and takes mitigating steps including use of collision resistant hash functions when deserializing dictionaries.
-The `UntrustedData` default mode merely hardens against some common attacks, but is no fully secure solution in itself.
-
-When you are deserializing data that you already trust and wish to use faster (insecure) hash functions, you can switch to trusted data mode by configuring your `MessagePackSerializerOptions.Security` property:
+When deserializing untrusted data, put MessagePack into a more secure mode by configuring your `MessagePackSerializerOptions.Security` property:
 
 ```cs
 var options = MessagePackSerializerOptions.Standard
-    .WithSecurity(MessagePackSecurity.TrustedData);
+    .WithSecurity(MessagePackSecurity.UntrustedData);
 
 // Pass the options explicitly for the greatest control.
 T object = MessagePackSerializer.Deserialize<T>(data, options);
@@ -683,8 +678,9 @@ T object = MessagePackSerializer.Deserialize<T>(data, options);
 MessagePackSerializer.DefaultOptions = options;
 ```
 
-You can also derive your own type from the `MessagePackSecurity` class to customize security decisions and hash functions.
-Provide an instance of your derived type to the `MessagePackSerializationOptions` type to use it in your deserialization.
+You should also avoid the Typeless serializer/formatters/resolvers for untrusted data as that opens the door for the untrusted data to potentially deserialize unanticipated types that can compromise security.
+
+The `UntrustedData` mode merely hardens against some common attacks, but is no fully secure solution in itself.
 
 ## Performance
 
diff --git a/doc/migrating_v2-v3.md b/doc/migrating_v2-v3.md
index 7dbdc9181..9d3569470 100644
--- a/doc/migrating_v2-v3.md
+++ b/doc/migrating_v2-v3.md
@@ -9,10 +9,6 @@ v3 adds many new diagnostic providers to the set of analyzers as well, with gene
 
 ## Breaking Changes
 
-- Secure by default: `MessagePackSerializerOptions.Security` defaults to `MessagePackSecurity.UntrustedData` instead of the v2 default of `MessagePackSecurity.TrustedData`.
-  This may have a small negative perf impact for deserialization.
-  It may also cause deserialization to fail if the object model requires hashing deserialized data for which no collision resistant hash function is known by the library.
-  [Learn more about security switches and customization](../README.md#security).
 - `MessagePackAnalyzer.json` is no longer used to configure the analyzer.
   Use `GeneratedMessagePackResolverAttribute`, `MessagePackKnownFormatterAttribute` and `MessagePackAssumedFormattableAttribute` instead.
 - The `mpc` CLI tool is no longer used to generate ahead-of-time (AOT) formatters and resolver.
diff --git a/src/MessagePack/MessagePackSecurity.cs b/src/MessagePack/MessagePackSecurity.cs
index 3a6715299..ecbd99c99 100644
--- a/src/MessagePack/MessagePackSecurity.cs
+++ b/src/MessagePack/MessagePackSecurity.cs
@@ -20,7 +20,8 @@ namespace MessagePack
     public class MessagePackSecurity
     {
         /// <summary>
-        /// Gets an instance preconfigured with settings that omit all protections. Useful for deserializing fully-trusted and valid msgpack sequences.
+        /// Gets an instance preconfigured with settings that omit hash collision resistance protections.
+        /// Useful for deserializing fully-trusted and valid msgpack sequences.
         /// </summary>
         public static readonly MessagePackSecurity TrustedData = new MessagePackSecurity();
 
@@ -79,7 +80,7 @@ protected MessagePackSecurity(MessagePackSecurity copyFrom)
         /// Since stack space occupied may vary by the kind of object deserialized, a conservative value for this property to defend against stack overflow attacks might be 500.
         /// </para>
         /// </remarks>
-        public int MaximumObjectGraphDepth { get; private set; } = int.MaxValue;
+        public int MaximumObjectGraphDepth { get; private set; } = 500;
 
         /// <summary>
         /// Gets a copy of these options with the <see cref="MaximumObjectGraphDepth"/> property set to a new value.
diff --git a/src/MessagePack/MessagePackSerializerOptions.cs b/src/MessagePack/MessagePackSerializerOptions.cs
index db48a386d..f750f93df 100644
--- a/src/MessagePack/MessagePackSerializerOptions.cs
+++ b/src/MessagePack/MessagePackSerializerOptions.cs
@@ -135,7 +135,7 @@ protected MessagePackSerializerOptions(MessagePackSerializerOptions copyFrom)
         /// <value>
         /// The default value is to use <see cref="MessagePackSecurity.TrustedData"/>.
         /// </value>
-        public MessagePackSecurity Security { get; private set; } = MessagePackSecurity.UntrustedData;
+        public MessagePackSecurity Security { get; private set; } = MessagePackSecurity.TrustedData;
 
         /// <summary>
         /// Gets a thread-safe pool of reusable <see cref="Sequence{T}"/> objects.


Test patch:
diff --git a/tests/MessagePack.Tests/MessagePackSerializerOptionsTests.cs b/tests/MessagePack.Tests/MessagePackSerializerOptionsTests.cs
index 5b995f7b5..0db5157c0 100644
--- a/tests/MessagePack.Tests/MessagePackSerializerOptionsTests.cs
+++ b/tests/MessagePack.Tests/MessagePackSerializerOptionsTests.cs
@@ -75,8 +75,8 @@ public void Resolver()
     [Fact]
     public void Security()
     {
-        Assert.Same(MessagePackSecurity.UntrustedData, MessagePackSerializerOptions.Standard.Security);
-        Assert.Same(MessagePackSecurity.TrustedData, MessagePackSerializerOptions.Standard.WithSecurity(MessagePackSecurity.TrustedData).Security);
+        Assert.Same(MessagePackSecurity.TrustedData, MessagePackSerializerOptions.Standard.Security);
+        Assert.Same(MessagePackSecurity.UntrustedData, MessagePackSerializerOptions.Standard.WithSecurity(MessagePackSecurity.UntrustedData).Security);
     }
 
     [Fact]