Skip to content

Commit

Permalink
Add type filtering feature (#281)
Browse files Browse the repository at this point in the history
* Add type filtering feature

* fix implicit typed variable and lambda expression build error

* Add documentation

* Deserializer type filter should work on downcasted type

* add XML-DOC

* Update API Approval list
  • Loading branch information
Arkatufus authored Jan 11, 2022
1 parent 7a78155 commit 122f5af
Show file tree
Hide file tree
Showing 11 changed files with 281 additions and 26 deletions.
58 changes: 56 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,20 +48,74 @@ var serializer = new Serializer(options);

This is essential for frameworks like Akka.NET where we need to be able to resolve live Actor References in the deserializing system.

## Whitelisting Types On Deserialization

Sometimes we need to limit the types that are allowed to be deserialized for security reasons. For this reason, you can either pass a class instance that implements the `ITypeFilter` interface into the `SerializerOptions` or use the `TypeFilterBuilder` class to build a `TypeFilter` that Hyperion can use to filter out any possibly harmful injection attack during deserialization.

using the `ITypeFilter` interface:

```c#
public sealed class TypeFilter : ITypeFilter
{
public ImmutableHashSet<string> FilteredTypes { get; }

internal TypeFilter(IEnumerable<Type> types)
{
FilteredTypes = types.Select(t => t.GetShortAssemblyQualifiedName()).ToImmutableHashSet();
}

public bool IsAllowed(string typeName)
=> FilteredTypes.Any(t => t == typeName);
}
```

using the `TypeFilterBuilder` convenience builder:

```c#
var typeFilter = TypeFilterBuilder.Create()
.Include<AllowedClassA>()
.Include<AllowedClassB>()
.Build();

var options = SerializerOptions.Default
.WithTypeFilter(typeFilter);

var serializer = new Serializer(options);
```

### Convert Whitelist To Blacklist

To do blacklisting instead of whitelisting a list of types, you will need to do a slight modification to the TypeFilter class.

```c#
public sealed class TypeFilter : ITypeFilter
{
public ImmutableHashSet<string> FilteredTypes { get; }

internal TypeFilter(IEnumerable<Type> types)
{
FilteredTypes = types.Select(t => t.GetShortAssemblyQualifiedName()).ToImmutableHashSet();
}

public bool IsAllowed(string typeName)
=> FilteredTypes.All(t => t != typeName);
}
```

## Version Tolerance

Hyperion has been designed to work in multiple modes in terms of version tolerance vs. performance.

1. Pre Register Types, when using "Pre registered types", Hyperion will only emit a type ID in the output stream.
This results in the best performance, but is also fragile if different clients have different versions of the contract types.
2. Non Versioned, this is largely the same as the above, but the serializer does not need to know about your types up front. it will embed the fully qualified typename
in the outputstream. this results in a larger payload and some performance overhead.
in the output stream. this results in a larger payload and some performance overhead.
3. Versioned, in this mode, Hyperion will emit both type names and field information in the output stream.
This allows systems to have slightly different versions of the contract types where some fields may have been added or removed.

Hyperion has been designed as a wire format, point to point for soft realtime scenarios.
If you need a format that is durable for persistence over time.
e.g. EventSourcing or for message queues, then Protobuf or MS Bond is probably a better choise as those formats have been designed for true version tolerance.
e.g. EventSourcing or for message queues, then Protobuf or MS Bond is probably a better choice as those formats have been designed for true version tolerance.

## Performance

Expand Down
35 changes: 33 additions & 2 deletions src/Hyperion.API.Tests/CoreApiSpec.ApproveApi.approved.txt
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ namespace Hyperion
public void TrackDeserializedType([Hyperion.Internal.NotNull] System.Type type) { }
public void TrackDeserializedTypeWithVersion([Hyperion.Internal.NotNull] System.Type type, [Hyperion.Internal.NotNull] Hyperion.TypeVersionInfo versionInfo) { }
}
public sealed class DisabledTypeFilter : Hyperion.ITypeFilter
{
public static readonly Hyperion.DisabledTypeFilter Instance;
public bool IsAllowed(string typeName) { }
}
public delegate object FieldInfoReader(object obj);
public delegate void FieldInfoWriter(object obj, object value);
public delegate void FieldReader(System.IO.Stream stream, object obj, Hyperion.DeserializerSession session);
Expand All @@ -49,6 +54,10 @@ namespace Hyperion
{
void BuildSerializer(Hyperion.Serializer serializer, Hyperion.ValueSerializers.ObjectSerializer objectSerializer);
}
public interface ITypeFilter
{
bool IsAllowed(string typeName);
}
[System.AttributeUsage(System.AttributeTargets.Field | System.AttributeTargets.All, AllowMultiple=false, Inherited=true)]
public sealed class IgnoreAttribute : System.Attribute
{
Expand Down Expand Up @@ -90,18 +99,24 @@ namespace Hyperion
public class SerializerOptions
{
public static readonly Hyperion.SerializerOptions Default;
[System.Obsolete]
[System.Obsolete("This constructor is deprecated and will be removed in the future, please use the " +
"one with the packageNameOverrides argument")]
public SerializerOptions(bool versionTolerance = false, bool preserveObjectReferences = false, System.Collections.Generic.IEnumerable<Hyperion.Surrogate> surrogates = null, System.Collections.Generic.IEnumerable<Hyperion.SerializerFactories.ValueSerializerFactory> serializerFactories = null, System.Collections.Generic.IEnumerable<System.Type> knownTypes = null, bool ignoreISerializable = false) { }
[System.Obsolete]
[System.Obsolete("This constructor is deprecated and will be removed in the future, please use the " +
"one with the disallowUnsafeTypes argument")]
public SerializerOptions(bool versionTolerance, bool preserveObjectReferences, System.Collections.Generic.IEnumerable<Hyperion.Surrogate> surrogates, System.Collections.Generic.IEnumerable<Hyperion.SerializerFactories.ValueSerializerFactory> serializerFactories, System.Collections.Generic.IEnumerable<System.Type> knownTypes, bool ignoreISerializable, System.Collections.Generic.IEnumerable<System.Func<string, string>> packageNameOverrides) { }
[System.Obsolete("This constructor is deprecated and will be removed in the future, please use the " +
"one with the typeFilter argument")]
public SerializerOptions(bool versionTolerance, bool preserveObjectReferences, System.Collections.Generic.IEnumerable<Hyperion.Surrogate> surrogates, System.Collections.Generic.IEnumerable<Hyperion.SerializerFactories.ValueSerializerFactory> serializerFactories, System.Collections.Generic.IEnumerable<System.Type> knownTypes, bool ignoreISerializable, System.Collections.Generic.IEnumerable<System.Func<string, string>> packageNameOverrides, bool disallowUnsafeTypes) { }
public SerializerOptions(bool versionTolerance, bool preserveObjectReferences, System.Collections.Generic.IEnumerable<Hyperion.Surrogate> surrogates, System.Collections.Generic.IEnumerable<Hyperion.SerializerFactories.ValueSerializerFactory> serializerFactories, System.Collections.Generic.IEnumerable<System.Type> knownTypes, bool ignoreISerializable, System.Collections.Generic.IEnumerable<System.Func<string, string>> packageNameOverrides, bool disallowUnsafeTypes, Hyperion.ITypeFilter typeFilter) { }
public Hyperion.SerializerOptions WithDisallowUnsafeType(bool disallowUnsafeType) { }
public Hyperion.SerializerOptions WithIgnoreSerializable(bool ignoreISerializable) { }
public Hyperion.SerializerOptions WithKnownTypes(System.Collections.Generic.IEnumerable<System.Type> knownTypes) { }
public Hyperion.SerializerOptions WithPackageNameOverrides(System.Collections.Generic.IEnumerable<System.Func<string, string>> packageNameOverrides) { }
public Hyperion.SerializerOptions WithPreserveObjectReferences(bool preserveObjectReferences) { }
public Hyperion.SerializerOptions WithSerializerFactory(System.Collections.Generic.IEnumerable<Hyperion.SerializerFactories.ValueSerializerFactory> serializerFactories) { }
public Hyperion.SerializerOptions WithSurrogates(System.Collections.Generic.IEnumerable<Hyperion.Surrogate> surrogates) { }
public Hyperion.SerializerOptions WithTypeFilter(Hyperion.ITypeFilter typeFilter) { }
public Hyperion.SerializerOptions WithVersionTolerance(bool versionTolerance) { }
}
public class SerializerSession
Expand Down Expand Up @@ -130,6 +145,18 @@ namespace Hyperion
{
public Surrogate(System.Func<TSource, TSurrogate> toSurrogate, System.Func<TSurrogate, TSource> fromSurrogate) { }
}
public sealed class TypeFilter : Hyperion.ITypeFilter
{
public System.Collections.Immutable.ImmutableHashSet<string> FilteredTypes { get; }
public bool IsAllowed(string typeName) { }
}
public class TypeFilterBuilder
{
public Hyperion.TypeFilter Build() { }
public Hyperion.TypeFilterBuilder Include(System.Type type) { }
public Hyperion.TypeFilterBuilder Include<T>() { }
public static Hyperion.TypeFilterBuilder Create() { }
}
public class TypeVersionInfo
{
public TypeVersionInfo() { }
Expand Down Expand Up @@ -591,6 +618,10 @@ namespace Hyperion.Internal
public Hyperion.Internal.ImplicitUseTargetFlags TargetFlags { get; }
public Hyperion.Internal.ImplicitUseKindFlags UseKindFlags { get; }
}
public class UserEvilDeserializationException : Hyperion.Internal.EvilDeserializationException
{
public UserEvilDeserializationException(string message, string typeString) { }
}
[System.AttributeUsage(System.AttributeTargets.Property | System.AttributeTargets.Field | System.AttributeTargets.Parameter | System.AttributeTargets.All)]
public sealed class ValueProviderAttribute : System.Attribute
{
Expand Down
63 changes: 62 additions & 1 deletion src/Hyperion.Tests/UnsafeDeserializationExclusionTests.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
using System.IO;
using System;
using System.IO;
using Hyperion.Extensions;
using Hyperion.Internal;
using Xunit;
using FluentAssertions;

namespace Hyperion.Tests
{
Expand All @@ -22,5 +24,64 @@ public void CantDeserializeANaughtyType()
serializer.Deserialize<DirectoryInfo>(stream));
}
}

internal class ClassA
{ }

internal class ClassB
{ }

internal class ClassC
{ }

[Fact]
public void TypeFilterShouldThrowOnNaughtyType()
{
var typeFilter = TypeFilterBuilder.Create()
.Include<ClassA>()
.Include<ClassB>()
.Build();

var options = SerializerOptions.Default
.WithTypeFilter(typeFilter);

var serializer = new Serializer(options);

using (var stream = new MemoryStream())
{
serializer.Serialize(new ClassA(), stream);
stream.Position = 0;
Action act = () => serializer.Deserialize<ClassA>(stream);
act.Should().NotThrow();

stream.Position = 0;
Action actObj = () => serializer.Deserialize<object>(stream);
actObj.Should().NotThrow();
}

using (var stream = new MemoryStream())
{
serializer.Serialize(new ClassB(), stream);
stream.Position = 0;
Action act = () => serializer.Deserialize<ClassB>(stream);
act.Should().NotThrow();

stream.Position = 0;
Action actObj = () => serializer.Deserialize<object>(stream);
actObj.Should().NotThrow();
}

using (var stream = new MemoryStream())
{
serializer.Serialize(new ClassC(), stream);
stream.Position = 0;
Action act = () => serializer.Deserialize<ClassC>(stream);
act.Should().Throw<UserEvilDeserializationException>();

stream.Position = 0;
Action actObj = () => serializer.Deserialize<object>(stream);
actObj.Should().Throw<UserEvilDeserializationException>();
}
}
}
}
13 changes: 8 additions & 5 deletions src/Hyperion/Extensions/TypeEx.cs
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,8 @@ private static Type GetTypeFromManifestName(Stream stream, DeserializerSession s
break;
}
return LoadTypeByName(shortName, session.Serializer.Options.DisallowUnsafeTypes);
var options = session.Serializer.Options;
return LoadTypeByName(shortName, options.DisallowUnsafeTypes, options.TypeFilter);
});
}

Expand Down Expand Up @@ -192,12 +193,14 @@ private static bool UnsafeInheritanceCheck(Type type)
return false;
}

public static Type LoadTypeByName(string name, bool disallowUnsafeTypes)
public static Type LoadTypeByName(string name, bool disallowUnsafeTypes, ITypeFilter typeFilter)
{
if (disallowUnsafeTypes && UnsafeTypesDenySet.Any(name.Contains))
if (disallowUnsafeTypes)
{
throw new EvilDeserializationException(
"Unsafe Type Deserialization Detected!", name);
if(UnsafeTypesDenySet.Any(name.Contains))
throw new EvilDeserializationException("Unsafe Type Deserialization Detected!", name);
if(!typeFilter.IsAllowed(name))
throw new UserEvilDeserializationException("Unsafe Type Deserialization Detected!", name);
}
try
{
Expand Down
1 change: 1 addition & 0 deletions src/Hyperion/Hyperion.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="System.Collections.Immutable" Version="5.0.0" />
<PackageReference Include="System.Runtime.Serialization.Primitives" Version="4.3.0" />
</ItemGroup>

Expand Down
15 changes: 15 additions & 0 deletions src/Hyperion/ITypeFilter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
namespace Hyperion
{
/// <summary>
/// Provide a callback to allow a user defined Type filter during certain operations
/// </summary>
public interface ITypeFilter
{
/// <summary>
/// Determines if a fully qualified class name is allowed to be processed or not
/// </summary>
/// <param name="typeName">The fully qualified class name of the type to be filtered</param>
/// <returns><c>true</c> if a type is allowed</returns>
bool IsAllowed(string typeName);
}
}
7 changes: 7 additions & 0 deletions src/Hyperion/Internal/Annotations.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,13 @@ public EvilDeserializationException(string message,

public string BadTypeString { get; }
}

public class UserEvilDeserializationException : EvilDeserializationException
{
public UserEvilDeserializationException(string message, string typeString) : base(message, typeString)
{ }
}

/// <summary>
/// Indicates that the value of the marked element could be <c>null</c> sometimes,
/// so the check for <c>null</c> is necessary before its usage.
Expand Down
Loading

0 comments on commit 122f5af

Please sign in to comment.