// Copyright (c) All contributors. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; using System.Collections; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Runtime.ExceptionServices; using MessagePack.Formatters; using MessagePack.Internal; namespace MessagePack { /// /// Settings related to security, particularly relevant when deserializing data from untrusted sources. /// public class MessagePackSecurity { /// /// Gets an instance preconfigured with settings that omit all protections. Useful for deserializing fully-trusted and valid msgpack sequences. /// public static readonly MessagePackSecurity TrustedData = new MessagePackSecurity(); /// /// Gets an instance preconfigured with protections applied with reasonable settings for deserializing untrusted msgpack sequences. /// public static readonly MessagePackSecurity UntrustedData = new MessagePackSecurity { HashCollisionResistant = true, MaximumObjectGraphDepth = 500, }; private readonly ObjectFallbackEqualityComparer objectFallbackEqualityComparer; private MessagePackSecurity() { this.objectFallbackEqualityComparer = new ObjectFallbackEqualityComparer(this); } /// /// Initializes a new instance of the class /// with properties copied from a provided template. /// /// The template to copy from. protected MessagePackSecurity(MessagePackSecurity copyFrom) : this() { if (copyFrom is null) { throw new ArgumentNullException(nameof(copyFrom)); } this.HashCollisionResistant = copyFrom.HashCollisionResistant; this.MaximumObjectGraphDepth = copyFrom.MaximumObjectGraphDepth; } /// /// Gets a value indicating whether data to be deserialized is untrusted and thus should not be allowed to create /// dictionaries or other hash-based collections unless the hashed type has a hash collision resistant implementation available. /// This can mitigate some denial of service attacks when deserializing untrusted code. /// /// /// The value is false for and true for . /// public bool HashCollisionResistant { get; private set; } /// /// Gets the maximum depth of an object graph that may be deserialized. /// /// /// /// This value can be reduced to avoid a stack overflow that would crash the process when deserializing a msgpack sequence designed to cause deep recursion. /// A very short callstack on a thread with 1MB of total stack space might deserialize ~2000 nested arrays before crashing due to a stack overflow. /// 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. /// /// public int MaximumObjectGraphDepth { get; private set; } = int.MaxValue; /// /// Gets a copy of these options with the property set to a new value. /// /// The new value for the property. /// The new instance; or the original if the value is unchanged. public MessagePackSecurity WithMaximumObjectGraphDepth(int maximumObjectGraphDepth) { if (this.MaximumObjectGraphDepth == maximumObjectGraphDepth) { return this; } var clone = this.Clone(); clone.MaximumObjectGraphDepth = maximumObjectGraphDepth; return clone; } /// /// Gets a copy of these options with the property set to a new value. /// /// The new value for the property. /// The new instance; or the original if the value is unchanged. public MessagePackSecurity WithHashCollisionResistant(bool hashCollisionResistant) { if (this.HashCollisionResistant == hashCollisionResistant) { return this; } var clone = this.Clone(); clone.HashCollisionResistant = hashCollisionResistant; return clone; } /// /// Gets an that is suitable to use with a hash-based collection. /// /// The type of key that will be hashed in the collection. /// The to use. /// /// When is active, this will be a collision resistant instance which may reject certain key types. /// When is not active, this will be . /// public IEqualityComparer GetEqualityComparer() { return this.HashCollisionResistant ? GetHashCollisionResistantEqualityComparer() : EqualityComparer.Default; } /// /// Gets an that is suitable to use with a hash-based collection. /// /// The to use. /// /// When is active, this will be a collision resistant instance which may reject certain key types. /// When is not active, this will be . /// public IEqualityComparer GetEqualityComparer() { return this.HashCollisionResistant ? GetHashCollisionResistantEqualityComparer() : EqualityComparer.Default; } /// /// Returns a hash collision resistant equality comparer. /// /// The type of key that will be hashed in the collection. /// A hash collision resistant equality comparer. protected virtual IEqualityComparer GetHashCollisionResistantEqualityComparer() { IEqualityComparer result = null; if (typeof(T).GetTypeInfo().IsEnum) { Type underlyingType = typeof(T).GetTypeInfo().GetEnumUnderlyingType(); result = underlyingType == typeof(sbyte) ? CollisionResistantHasher.Instance : underlyingType == typeof(byte) ? CollisionResistantHasher.Instance : underlyingType == typeof(short) ? CollisionResistantHasher.Instance : underlyingType == typeof(ushort) ? CollisionResistantHasher.Instance : underlyingType == typeof(int) ? CollisionResistantHasher.Instance : underlyingType == typeof(uint) ? CollisionResistantHasher.Instance : null; } else { // For anything 32-bits and under, our fallback base secure hasher is usually adequate since it makes the hash unpredictable. // We should have special implementations for any value that is larger than 32-bits in order to make sure // that all the data gets hashed securely rather than trivially and predictably compressed into 32-bits before being hashed. // We also have to specially handle some 32-bit types (e.g. float) where multiple in-memory representations should hash to the same value. // Any type supported by the PrimitiveObjectFormatter should be added here if supporting it as a key in a collection makes sense. result = // 32-bits or smaller: typeof(T) == typeof(bool) ? CollisionResistantHasher.Instance : typeof(T) == typeof(char) ? CollisionResistantHasher.Instance : typeof(T) == typeof(sbyte) ? CollisionResistantHasher.Instance : typeof(T) == typeof(byte) ? CollisionResistantHasher.Instance : typeof(T) == typeof(short) ? CollisionResistantHasher.Instance : typeof(T) == typeof(ushort) ? CollisionResistantHasher.Instance : typeof(T) == typeof(int) ? CollisionResistantHasher.Instance : typeof(T) == typeof(uint) ? CollisionResistantHasher.Instance : // Larger than 32-bits (or otherwise require special handling): typeof(T) == typeof(long) ? (IEqualityComparer)Int64EqualityComparer.Instance : typeof(T) == typeof(ulong) ? (IEqualityComparer)UInt64EqualityComparer.Instance : typeof(T) == typeof(float) ? (IEqualityComparer)SingleEqualityComparer.Instance : typeof(T) == typeof(double) ? (IEqualityComparer)DoubleEqualityComparer.Instance : typeof(T) == typeof(string) ? (IEqualityComparer)StringEqualityComparer.Instance : typeof(T) == typeof(Guid) ? (IEqualityComparer)GuidEqualityComparer.Instance : typeof(T) == typeof(DateTime) ? (IEqualityComparer)DateTimeEqualityComparer.Instance : typeof(T) == typeof(DateTimeOffset) ? (IEqualityComparer)DateTimeOffsetEqualityComparer.Instance : typeof(T) == typeof(object) ? (IEqualityComparer)this.objectFallbackEqualityComparer : null; } // Any type we don't explicitly whitelist here shouldn't be allowed to use as the key in a hash-based collection since it isn't known to be hash resistant. // This method can of course be overridden to add more hash collision resistant type support, or the deserializing party can indicate that the data is Trusted // so that this method doesn't even get called. return result ?? throw new TypeAccessException($"No hash-resistant equality comparer available for type: {typeof(T)}"); } /// /// Checks the depth of the deserializing graph and increments it by 1. /// /// The reader that is involved in deserialization. /// /// Callers should decrement after exiting that edge in the graph. /// /// Thrown if is already at or exceeds . /// /// Rather than wrap the body of every method, /// this should wrap *calls* to these methods. They need not appear in pure "thunk" methods that simply delegate the deserialization to another formatter. /// In this way, we can avoid repeatedly incrementing and decrementing the counter when deserializing each element of a collection. /// public void DepthStep(ref MessagePackReader reader) { if (reader.Depth >= this.MaximumObjectGraphDepth) { throw new InsufficientExecutionStackException($"This msgpack sequence has an object graph that exceeds the maximum depth allowed of {MaximumObjectGraphDepth}."); } reader.Depth++; } /// /// Returns a hash collision resistant equality comparer. /// /// A hash collision resistant equality comparer. protected virtual IEqualityComparer GetHashCollisionResistantEqualityComparer() => (IEqualityComparer)this.GetHashCollisionResistantEqualityComparer(); /// /// Creates a new instance that is a copy of this one. /// /// /// Derived types should override this method to instantiate their own derived type. /// protected virtual MessagePackSecurity Clone() => new MessagePackSecurity(this); /// /// A hash collision resistant implementation of . /// /// The type of key that will be hashed. private class CollisionResistantHasher : IEqualityComparer, IEqualityComparer { internal static readonly CollisionResistantHasher Instance = new CollisionResistantHasher(); public bool Equals(T x, T y) => EqualityComparer.Default.Equals(x, y); bool IEqualityComparer.Equals(object x, object y) => ((IEqualityComparer)EqualityComparer.Default).Equals(x, y); public int GetHashCode(object obj) => this.GetHashCode((T)obj); public virtual int GetHashCode(T value) => HashCode.Combine(value); } /// /// A special hash-resistent equality comparer that defers picking the actual implementation /// till it can check the runtime type of each value to be hashed. /// private class ObjectFallbackEqualityComparer : IEqualityComparer, IEqualityComparer { private static readonly Lazy GetHashCollisionResistantEqualityComparerOpenGenericMethod = new Lazy(() => typeof(MessagePackSecurity).GetTypeInfo().DeclaredMethods.Single(m => m.Name == nameof(MessagePackSecurity.GetHashCollisionResistantEqualityComparer) && m.IsGenericMethod)); private readonly MessagePackSecurity security; private readonly ThreadsafeTypeKeyHashTable equalityComparerCache = new ThreadsafeTypeKeyHashTable(); internal ObjectFallbackEqualityComparer(MessagePackSecurity security) { this.security = security ?? throw new ArgumentNullException(nameof(security)); } bool IEqualityComparer.Equals(object x, object y) => EqualityComparer.Default.Equals(x, y); bool IEqualityComparer.Equals(object x, object y) => ((IEqualityComparer)EqualityComparer.Default).Equals(x, y); public int GetHashCode(object value) { if (value is null) { return 0; } Type valueType = value.GetType(); // Take care to avoid recursion. if (valueType == typeof(object)) { // We can trust object.GetHashCode() to be collision resistant. return value.GetHashCode(); } if (!equalityComparerCache.TryGetValue(valueType, out IEqualityComparer equalityComparer)) { try { equalityComparer = (IEqualityComparer)GetHashCollisionResistantEqualityComparerOpenGenericMethod.Value.MakeGenericMethod(valueType).Invoke(this.security, Array.Empty()); } catch (TargetInvocationException ex) { ExceptionDispatchInfo.Capture(ex.InnerException).Throw(); } equalityComparerCache.TryAdd(valueType, equalityComparer); } return equalityComparer.GetHashCode(value); } } private class UInt64EqualityComparer : CollisionResistantHasher { internal static new readonly UInt64EqualityComparer Instance = new UInt64EqualityComparer(); public override int GetHashCode(ulong value) => HashCode.Combine((uint)(value >> 32), unchecked((uint)value)); } private class Int64EqualityComparer : CollisionResistantHasher { internal static new readonly Int64EqualityComparer Instance = new Int64EqualityComparer(); public override int GetHashCode(long value) => HashCode.Combine((int)(value >> 32), unchecked((int)value)); } private class SingleEqualityComparer : CollisionResistantHasher { internal static new readonly SingleEqualityComparer Instance = new SingleEqualityComparer(); public override unsafe int GetHashCode(float value) { // Special check for 0.0 so that the hash of 0.0 and -0.0 will equal. if (value == 0.0f) { return HashCode.Combine(0); } // Standardize on the binary representation of NaN prior to hashing. if (float.IsNaN(value)) { value = float.NaN; } int l = *(int*)&value; return l; } } private class DoubleEqualityComparer : CollisionResistantHasher { internal static new readonly DoubleEqualityComparer Instance = new DoubleEqualityComparer(); public override unsafe int GetHashCode(double value) { // Special check for 0.0 so that the hash of 0.0 and -0.0 will equal. if (value == 0.0) { return HashCode.Combine(0); } // Standardize on the binary representation of NaN prior to hashing. if (double.IsNaN(value)) { value = double.NaN; } long l = *(long*)&value; return HashCode.Combine((int)(l >> 32), unchecked((int)l)); } } private class GuidEqualityComparer : CollisionResistantHasher { internal static new readonly GuidEqualityComparer Instance = new GuidEqualityComparer(); public override unsafe int GetHashCode(Guid value) { var hash = default(HashCode); int* pGuid = (int*)&value; for (int i = 0; i < sizeof(Guid) / sizeof(int); i++) { hash.Add(pGuid[i]); } return hash.ToHashCode(); } } private class StringEqualityComparer : CollisionResistantHasher { internal static new readonly StringEqualityComparer Instance = new StringEqualityComparer(); public override int GetHashCode(string value) { #if NETCOREAPP // .NET Core already has a secure string hashing function. Just use it. return value?.GetHashCode() ?? 0; #else var hash = default(HashCode); for (int i = 0; i < value.Length; i++) { hash.Add(value[i]); } return hash.ToHashCode(); #endif } } private class DateTimeEqualityComparer : CollisionResistantHasher { internal static new readonly DateTimeEqualityComparer Instance = new DateTimeEqualityComparer(); public override unsafe int GetHashCode(DateTime value) => HashCode.Combine((int)(value.Ticks >> 32), unchecked((int)value.Ticks), value.Kind); } private class DateTimeOffsetEqualityComparer : CollisionResistantHasher { internal static new readonly DateTimeOffsetEqualityComparer Instance = new DateTimeOffsetEqualityComparer(); public override unsafe int GetHashCode(DateTimeOffset value) => HashCode.Combine((int)(value.UtcTicks >> 32), unchecked((int)value.UtcTicks)); } } }