Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add secure hashing for enums with backing integers of 32-bit or less #813

Merged
merged 4 commits into from Feb 10, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
80 changes: 49 additions & 31 deletions src/MessagePack/MessagePackSecurity.cs
Expand Up @@ -56,6 +56,7 @@ private MessagePackSecurity()
/// </summary>
/// <param name="copyFrom">The template to copy from.</param>
protected MessagePackSecurity(MessagePackSecurity copyFrom)
: this()
{
if (copyFrom is null)
{
Expand Down Expand Up @@ -156,37 +157,54 @@ public IEqualityComparer GetEqualityComparer()
/// <returns>A hash collision resistant equality comparer.</returns>
protected virtual IEqualityComparer<T> GetHashCollisionResistantEqualityComparer<T>()
{
// 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.
return
// 32-bits or smaller:
typeof(T) == typeof(bool) ? CollisionResistantHasher<T>.Instance :
typeof(T) == typeof(char) ? CollisionResistantHasher<T>.Instance :
typeof(T) == typeof(sbyte) ? CollisionResistantHasher<T>.Instance :
typeof(T) == typeof(byte) ? CollisionResistantHasher<T>.Instance :
typeof(T) == typeof(short) ? CollisionResistantHasher<T>.Instance :
typeof(T) == typeof(ushort) ? CollisionResistantHasher<T>.Instance :
typeof(T) == typeof(int) ? CollisionResistantHasher<T>.Instance :
typeof(T) == typeof(uint) ? CollisionResistantHasher<T>.Instance :

// Larger than 32-bits (or otherwise require special handling):
typeof(T) == typeof(long) ? (IEqualityComparer<T>)Int64EqualityComparer.Instance :
typeof(T) == typeof(ulong) ? (IEqualityComparer<T>)UInt64EqualityComparer.Instance :
typeof(T) == typeof(float) ? (IEqualityComparer<T>)SingleEqualityComparer.Instance :
typeof(T) == typeof(double) ? (IEqualityComparer<T>)DoubleEqualityComparer.Instance :
typeof(T) == typeof(string) ? (IEqualityComparer<T>)StringEqualityComparer.Instance :
typeof(T) == typeof(Guid) ? (IEqualityComparer<T>)GuidEqualityComparer.Instance :
typeof(T) == typeof(DateTime) ? (IEqualityComparer<T>)DateTimeEqualityComparer.Instance :
typeof(T) == typeof(DateTimeOffset) ? (IEqualityComparer<T>)DateTimeOffsetEqualityComparer.Instance :
typeof(T) == typeof(object) ? (IEqualityComparer<T>)this.objectFallbackEqualityComparer :

// 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.
throw new TypeAccessException($"No hash-resistant equality comparer available for type: {typeof(T)}");
IEqualityComparer<T> result = null;
if (typeof(T).GetTypeInfo().IsEnum)
{
Type underlyingType = typeof(T).GetTypeInfo().GetEnumUnderlyingType();
result =
underlyingType == typeof(sbyte) ? CollisionResistantHasher<T>.Instance :
underlyingType == typeof(byte) ? CollisionResistantHasher<T>.Instance :
underlyingType == typeof(short) ? CollisionResistantHasher<T>.Instance :
underlyingType == typeof(ushort) ? CollisionResistantHasher<T>.Instance :
underlyingType == typeof(int) ? CollisionResistantHasher<T>.Instance :
underlyingType == typeof(uint) ? CollisionResistantHasher<T>.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<T>.Instance :
typeof(T) == typeof(char) ? CollisionResistantHasher<T>.Instance :
typeof(T) == typeof(sbyte) ? CollisionResistantHasher<T>.Instance :
typeof(T) == typeof(byte) ? CollisionResistantHasher<T>.Instance :
typeof(T) == typeof(short) ? CollisionResistantHasher<T>.Instance :
typeof(T) == typeof(ushort) ? CollisionResistantHasher<T>.Instance :
typeof(T) == typeof(int) ? CollisionResistantHasher<T>.Instance :
typeof(T) == typeof(uint) ? CollisionResistantHasher<T>.Instance :

// Larger than 32-bits (or otherwise require special handling):
typeof(T) == typeof(long) ? (IEqualityComparer<T>)Int64EqualityComparer.Instance :
typeof(T) == typeof(ulong) ? (IEqualityComparer<T>)UInt64EqualityComparer.Instance :
typeof(T) == typeof(float) ? (IEqualityComparer<T>)SingleEqualityComparer.Instance :
typeof(T) == typeof(double) ? (IEqualityComparer<T>)DoubleEqualityComparer.Instance :
typeof(T) == typeof(string) ? (IEqualityComparer<T>)StringEqualityComparer.Instance :
typeof(T) == typeof(Guid) ? (IEqualityComparer<T>)GuidEqualityComparer.Instance :
typeof(T) == typeof(DateTime) ? (IEqualityComparer<T>)DateTimeEqualityComparer.Instance :
typeof(T) == typeof(DateTimeOffset) ? (IEqualityComparer<T>)DateTimeOffsetEqualityComparer.Instance :
typeof(T) == typeof(object) ? (IEqualityComparer<T>)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)}");
}

/// <summary>
Expand Down
55 changes: 55 additions & 0 deletions tests/MessagePack.Tests/MessagePackSecurityTests.cs
Expand Up @@ -106,6 +106,22 @@ public unsafe void EqualityComparer_Double()
Assert.NotEqual(eq.GetHashCode(1.0), eq.GetHashCode(2.0));
}

[Fact]
public void EqualityComparer_Enums()
{
Assert.NotNull(MessagePackSecurity.UntrustedData.GetEqualityComparer<SomeInt8Enum>());
Assert.NotNull(MessagePackSecurity.UntrustedData.GetEqualityComparer<SomeUInt8Enum>());
Assert.NotNull(MessagePackSecurity.UntrustedData.GetEqualityComparer<SomeInt16Enum>());
Assert.NotNull(MessagePackSecurity.UntrustedData.GetEqualityComparer<SomeUInt16Enum>());
Assert.NotNull(MessagePackSecurity.UntrustedData.GetEqualityComparer<SomeInt32Enum>());
Assert.NotNull(MessagePackSecurity.UntrustedData.GetEqualityComparer<SomeUInt32Enum>());

// Supporting enums with backing integers that exceed 32-bits would likely require Ref.Emit of new types
// since C# doesn't let us cast T to the underlying int type.
Assert.Throws<TypeAccessException>(() => MessagePackSecurity.UntrustedData.GetEqualityComparer<SomeInt64Enum>());
Assert.Throws<TypeAccessException>(() => MessagePackSecurity.UntrustedData.GetEqualityComparer<SomeUInt64Enum>());
}

[Fact]
public void EqualityComparer_ObjectFallback()
{
Expand All @@ -125,6 +141,13 @@ public void EqualityComparer_ObjectFallback()
Assert.NotEqual(eq.GetHashCode(o), eq.GetHashCode(new object()));
}

[Fact]
public void EqualityComparer_ObjectFallback_AfterCopyCtor()
{
var security = MessagePackSecurity.UntrustedData.WithMaximumObjectGraphDepth(15);
Assert.NotNull(security.GetEqualityComparer<object>());
}

/// <summary>
/// Verifies that arbitrary other types not known to be hash safe will be rejected.
/// </summary>
Expand Down Expand Up @@ -190,4 +213,36 @@ public void TypelessFormatterWithUntrustedData_UnsafeKeys()
public class ArbitraryType
{
}

public enum SomeInt8Enum : sbyte
{
}

public enum SomeUInt8Enum : byte
{
}

public enum SomeInt16Enum : short
{
}

public enum SomeUInt16Enum : ushort
{
}

public enum SomeInt32Enum : int
{
}

public enum SomeUInt32Enum : uint
{
}

public enum SomeInt64Enum : long
{
}

public enum SomeUInt64Enum : ulong
{
}
}