* 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
This commit is contained in:
Gregorius Soedharmo 2022-01-12 03:48:50 +07:00 коммит произвёл GitHub
Родитель 7a781559c9
Коммит 122f5af36d
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
11 изменённых файлов: 282 добавлений и 27 удалений

Просмотреть файл

@ -48,6 +48,60 @@ 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.
@ -55,13 +109,13 @@ Hyperion has been designed to work in multiple modes in terms of version toleran
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

Просмотреть файл

@ -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);
@ -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
{
@ -90,11 +99,16 @@ 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) { }
@ -102,6 +116,7 @@ namespace Hyperion
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
@ -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() { }
@ -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
{

Просмотреть файл

@ -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
{
@ -22,5 +24,64 @@ namespace Hyperion.Tests
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>();
}
}
}
}

Просмотреть файл

@ -162,7 +162,8 @@ namespace Hyperion.Extensions
break;
}
return LoadTypeByName(shortName, session.Serializer.Options.DisallowUnsafeTypes);
var options = session.Serializer.Options;
return LoadTypeByName(shortName, options.DisallowUnsafeTypes, options.TypeFilter);
});
}
@ -192,12 +193,14 @@ namespace Hyperion.Extensions
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
{

Просмотреть файл

@ -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>

Просмотреть файл

@ -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);
}
}

Просмотреть файл

@ -30,6 +30,13 @@ namespace Hyperion.Internal
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.

Просмотреть файл

@ -16,15 +16,7 @@ namespace Hyperion
{
public class SerializerOptions
{
public static readonly SerializerOptions Default = new SerializerOptions(
versionTolerance: false,
preserveObjectReferences: false,
surrogates: null,
serializerFactories: null,
knownTypes: null,
ignoreISerializable: false,
packageNameOverrides: null,
disallowUnsafeTypes: true);
public static readonly SerializerOptions Default = new SerializerOptions();
internal static List<Func<string, string>> DefaultPackageNameOverrides()
{
@ -81,8 +73,9 @@ namespace Hyperion
internal readonly Dictionary<Type, ushort> KnownTypesDict = new Dictionary<Type, ushort>();
internal readonly List<Func<string, string>> CrossFrameworkPackageNameOverrides = DefaultPackageNameOverrides();
internal readonly bool DisallowUnsafeTypes;
[Obsolete]
internal readonly ITypeFilter TypeFilter;
[Obsolete(message:"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,
@ -93,7 +86,7 @@ namespace Hyperion
: this(versionTolerance, preserveObjectReferences, surrogates, serializerFactories, knownTypes, ignoreISerializable, null)
{ }
[Obsolete]
[Obsolete(message:"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,
@ -102,7 +95,20 @@ namespace Hyperion
IEnumerable<Type> knownTypes,
bool ignoreISerializable,
IEnumerable<Func<string, string>> packageNameOverrides)
: this(versionTolerance, preserveObjectReferences, surrogates, serializerFactories, knownTypes, ignoreISerializable, null, true)
: this(versionTolerance, preserveObjectReferences, surrogates, serializerFactories, knownTypes, ignoreISerializable, packageNameOverrides, true)
{ }
[Obsolete(message:"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,
IEnumerable<Surrogate> surrogates,
IEnumerable<ValueSerializerFactory> serializerFactories,
IEnumerable<Type> knownTypes,
bool ignoreISerializable,
IEnumerable<Func<string, string>> packageNameOverrides,
bool disallowUnsafeTypes)
: this(versionTolerance, preserveObjectReferences, surrogates, serializerFactories, knownTypes, ignoreISerializable, packageNameOverrides, disallowUnsafeTypes, DisabledTypeFilter.Instance)
{ }
public SerializerOptions(
@ -113,7 +119,8 @@ namespace Hyperion
IEnumerable<Type> knownTypes,
bool ignoreISerializable,
IEnumerable<Func<string, string>> packageNameOverrides,
bool disallowUnsafeTypes)
bool disallowUnsafeTypes,
ITypeFilter typeFilter)
{
VersionTolerance = versionTolerance;
Surrogates = surrogates?.ToArray() ?? EmptySurrogates;
@ -136,6 +143,7 @@ namespace Hyperion
CrossFrameworkPackageNameOverrides.AddRange(packageNameOverrides);
DisallowUnsafeTypes = disallowUnsafeTypes;
TypeFilter = typeFilter ?? DisabledTypeFilter.Instance;
}
public SerializerOptions WithVersionTolerance(bool versionTolerance)
@ -154,6 +162,8 @@ namespace Hyperion
=> Copy(packageNameOverrides: packageNameOverrides);
public SerializerOptions WithDisallowUnsafeType(bool disallowUnsafeType)
=> Copy(disallowUnsafeType: disallowUnsafeType);
public SerializerOptions WithTypeFilter(ITypeFilter typeFilter)
=> Copy(typeFilter: typeFilter);
private SerializerOptions Copy(
bool? versionTolerance = null,
@ -163,7 +173,8 @@ namespace Hyperion
IEnumerable<Type> knownTypes = null,
bool? ignoreISerializable = null,
IEnumerable<Func<string, string>> packageNameOverrides = null,
bool? disallowUnsafeType = null)
bool? disallowUnsafeType = null,
ITypeFilter typeFilter = null)
=> new SerializerOptions(
versionTolerance ?? VersionTolerance,
preserveObjectReferences ?? PreserveObjectReferences,
@ -172,7 +183,8 @@ namespace Hyperion
knownTypes ?? KnownTypes,
ignoreISerializable ?? IgnoreISerializable,
packageNameOverrides ?? CrossFrameworkPackageNameOverrides,
disallowUnsafeType ?? DisallowUnsafeTypes
disallowUnsafeType ?? DisallowUnsafeTypes,
typeFilter ?? TypeFilter
);
}
}

Просмотреть файл

@ -0,0 +1,34 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using Hyperion.Extensions;
namespace Hyperion
{
/// <inheritdoc/>
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);
}
/// <summary>
/// A disabled type filter that always returns true
/// </summary>
public sealed class DisabledTypeFilter : ITypeFilter
{
public static readonly DisabledTypeFilter Instance = new DisabledTypeFilter();
private DisabledTypeFilter() { }
public bool IsAllowed(string typeName) => true;
}
}

Просмотреть файл

@ -0,0 +1,36 @@
using System;
using System.Collections.Generic;
namespace Hyperion
{
/// <summary>
/// Helper class to programatically create a <see cref="TypeFilter"/> using fluent builder pattern.
/// </summary>
public class TypeFilterBuilder
{
/// <summary>
/// Create a new instance of <see cref="TypeFilterBuilder"/>
/// </summary>
/// <returns>a new instance of <see cref="TypeFilterBuilder"/> </returns>
public static TypeFilterBuilder Create() => new TypeFilterBuilder();
private readonly List<Type> _types = new List<Type>();
private TypeFilterBuilder()
{ }
public TypeFilterBuilder Include<T>()
{
return Include(typeof(T));
}
public TypeFilterBuilder Include(Type type)
{
_types.Add(type);
return this;
}
public TypeFilter Build()
=> new TypeFilter(_types);
}
}

Просмотреть файл

@ -69,8 +69,9 @@ namespace Hyperion.ValueSerializers
if (shortname == null)
return null;
var options = session.Serializer.Options;
var type = TypeNameLookup.GetOrAdd(shortname,
name => TypeEx.LoadTypeByName(shortname, session.Serializer.Options.DisallowUnsafeTypes));
name => TypeEx.LoadTypeByName(shortname, options.DisallowUnsafeTypes, options.TypeFilter));
//add the deserialized type to lookup
if (session.Serializer.Options.PreserveObjectReferences)