diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 986e18d2..dfb542e4 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,3 +1,12 @@ +### 0.12.2 March 31 2022 #### + +* Fix [deserialization type cache was shared between multiple serializer instances](https://github.com/akkadotnet/Hyperion/pull/306) + +### 0.12.1 March 23 2022 #### + +* Fix disallow-unsafe-type Akka.NET settings and harden unsafe type detection [301](https://github.com/akkadotnet/Hyperion/pull/301) +* Bump [Akka version from 1.4.34 to 1.4.35](https://github.com/akkadotnet/akka.net/releases/tag/1.4.35) + ### 0.12.0 January 12 2022 #### * Allow explicit control over which types can be deserialized [#281](https://github.com/akkadotnet/Hyperion/pull/281) diff --git a/build-system/pr-validation.yaml b/build-system/pr-validation.yaml index 6a62781c..71133c5e 100644 --- a/build-system/pr-validation.yaml +++ b/build-system/pr-validation.yaml @@ -21,10 +21,19 @@ jobs: vmImage: 'windows-2019' scriptFileName: build.cmd scriptArgs: all + - template: azure-pipeline.template.yaml parameters: - name: 'linux_pr' - displayName: 'Linux PR Validation' + name: 'linux_pr_net_core' + displayName: 'Linux PR Validation (netcoreapp3.1)' vmImage: 'ubuntu-16.04' scriptFileName: ./build.sh - scriptArgs: all \ No newline at end of file + scriptArgs: runTestsNetCore + + - template: azure-pipeline.template.yaml + parameters: + name: 'linux_pr_net_5' + displayName: 'Linux PR Validation (net5.0)' + vmImage: 'ubuntu-16.04' + scriptFileName: ./build.sh + scriptArgs: runTestsNet diff --git a/build-system/windows-pr-validation.yaml b/build-system/windows-pr-validation.yaml index 5f14abd4..18a7e762 100644 --- a/build-system/windows-pr-validation.yaml +++ b/build-system/windows-pr-validation.yaml @@ -20,10 +20,19 @@ jobs: vmImage: 'windows-2019' scriptFileName: build.cmd scriptArgs: all + - template: azure-pipeline.template.yaml parameters: - name: 'linux_pr' - displayName: 'Linux PR Validation' + name: 'linux_pr_net_core' + displayName: 'Linux PR Validation (netcoreapp3.1)' vmImage: 'ubuntu-18.04' scriptFileName: ./build.sh - scriptArgs: all + scriptArgs: runTestsNetCore + +- template: azure-pipeline.template.yaml + parameters: + name: 'linux_pr_net_5' + displayName: 'Linux PR Validation (net5.0)' + vmImage: 'ubuntu-18.04' + scriptFileName: ./build.sh + scriptArgs: runTestsNet diff --git a/build.fsx b/build.fsx index 2b65f763..b1919251 100644 --- a/build.fsx +++ b/build.fsx @@ -71,6 +71,8 @@ Target "Clean" (fun _ -> CleanDirs !! "./**/bin" CleanDirs !! "./**/obj" + + CreateDir "bin/nuget" ) Target "AssemblyInfo" (fun _ -> diff --git a/src/Hyperion.API.Tests/Hyperion.API.Tests.csproj b/src/Hyperion.API.Tests/Hyperion.API.Tests.csproj index 47966af4..4b11f1fc 100644 --- a/src/Hyperion.API.Tests/Hyperion.API.Tests.csproj +++ b/src/Hyperion.API.Tests/Hyperion.API.Tests.csproj @@ -9,7 +9,7 @@ - + @@ -17,7 +17,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/src/Hyperion.Akka.Integration.Tests/Hyperion.Akka.Integration.Tests.csproj b/src/Hyperion.Akka.Integration.Tests/Hyperion.Akka.Integration.Tests.csproj index a0b6417d..baae8a32 100644 --- a/src/Hyperion.Akka.Integration.Tests/Hyperion.Akka.Integration.Tests.csproj +++ b/src/Hyperion.Akka.Integration.Tests/Hyperion.Akka.Integration.Tests.csproj @@ -19,7 +19,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/src/Hyperion.Akka.Integration.Tests/IntegrationSpec.cs b/src/Hyperion.Akka.Integration.Tests/IntegrationSpec.cs index aaf7ece9..2a2d6200 100644 --- a/src/Hyperion.Akka.Integration.Tests/IntegrationSpec.cs +++ b/src/Hyperion.Akka.Integration.Tests/IntegrationSpec.cs @@ -1,5 +1,12 @@ using System; using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Runtime.InteropServices; +using System.Runtime.Serialization; +using System.Security.Claims; +using System.Security.Principal; +using System.Threading.Tasks; using Akka.Actor; using Akka.Configuration; using Akka.Serialization; @@ -7,6 +14,7 @@ using Akka.TestKit; using Akka.TestKit.Xunit2; using FluentAssertions; +using Hyperion.Internal; using Xunit.Abstractions; using AkkaSerializer = Akka.Serialization.Serializer; @@ -72,6 +80,79 @@ public void Bugfix263_Akka_HyperionSerializer_should_serialize_ActorPath_list() deserialized.Destinations[0].Should().Be(deserialized.Destinations[1]); } + [Fact] + public async Task CanDeserializeANaughtyTypeWhenAllowed() + { + var config = ConfigurationFactory.ParseString(@" +akka { + serialize-messages = on + actor { + serializers { + hyperion = ""Akka.Serialization.HyperionSerializer, Akka.Serialization.Hyperion"" + } + serialization-bindings { + ""System.Object"" = hyperion + } + serialization-settings.hyperion.disallow-unsafe-type = false + } +}"); + var system = ActorSystem.Create("unsafeSystem", config); + + try + { + var serializer = system.Serialization.FindSerializerForType(typeof(DirectoryInfo)); + var di = new DirectoryInfo(@"c:\"); + + var serialized = serializer.ToBinary(di); + var deserialized = serializer.FromBinary(serialized); + } + finally + { + await system.Terminate(); + } + } + + [Fact] + public async Task CantDeserializeANaughtyTypeByDefault() + { + var config = ConfigurationFactory.ParseString(@" +akka { + serialize-messages = on + actor { + serializers { + hyperion = ""Akka.Serialization.HyperionSerializer, Akka.Serialization.Hyperion"" + } + serialization-bindings { + ""System.Object"" = hyperion + } + serialization-settings.hyperion.disallow-unsafe-type = true # this is the default value + } +}"); + var system = ActorSystem.Create("unsafeSystem", config); + + try + { + var deserializer = system.Serialization.FindSerializerForType(typeof(DirectoryInfo)); + var di = new DirectoryInfo(@"c:\"); + + byte[] serialized; + using (var stream = new MemoryStream()) + { + var serializer = new Serializer(SerializerOptions.Default.WithDisallowUnsafeType(false)); + serializer.Serialize(di, stream); + stream.Position = 0; + serialized = stream.ToArray(); + } + + var ex = Assert.Throws(() => deserializer.FromBinary(serialized)); + ex.InnerException.Should().BeOfType(); + } + finally + { + await system.Terminate(); + } + } + private class MyActor: ReceiveActor { diff --git a/src/Hyperion.Benchmarks/TypeRejectionBenchmark.cs b/src/Hyperion.Benchmarks/TypeRejectionBenchmark.cs new file mode 100644 index 00000000..7cda0a9f --- /dev/null +++ b/src/Hyperion.Benchmarks/TypeRejectionBenchmark.cs @@ -0,0 +1,46 @@ +using System; +using System.IO; +using BenchmarkDotNet.Attributes; +using Hyperion.Internal; + +namespace Hyperion.Benchmarks +{ + [Config(typeof(HyperionConfig))] + public class TypeRejectionBenchmark + { + private Serializer _serializer; + private Stream _dangerousStream; + + [GlobalSetup] + public void Setup() + { + var di = new DirectoryInfo("C:\\Windows\\Windows32"); + var serializer = new Serializer(SerializerOptions.Default.WithDisallowUnsafeType(false)); + _dangerousStream = new MemoryStream(); + serializer.Serialize(di, _dangerousStream); + + _serializer = new Serializer(); + } + + [GlobalCleanup] + public void Cleanup() + { + _dangerousStream.Dispose(); + } + + [Benchmark] + public void DeserializeDanger() + { + _dangerousStream.Position = 0; + try + { + _serializer.Deserialize(_dangerousStream); + } + catch(EvilDeserializationException) + { + // no-op + } + } + + } +} \ No newline at end of file diff --git a/src/Hyperion.Tests.FSharpData/Hyperion.Tests.FSharpData.fsproj b/src/Hyperion.Tests.FSharpData/Hyperion.Tests.FSharpData.fsproj index d8fa6533..2304331f 100644 --- a/src/Hyperion.Tests.FSharpData/Hyperion.Tests.FSharpData.fsproj +++ b/src/Hyperion.Tests.FSharpData/Hyperion.Tests.FSharpData.fsproj @@ -9,7 +9,7 @@ - + diff --git a/src/Hyperion.Tests/Hyperion.Tests.csproj b/src/Hyperion.Tests/Hyperion.Tests.csproj index 6983f5f3..1e934f67 100644 --- a/src/Hyperion.Tests/Hyperion.Tests.csproj +++ b/src/Hyperion.Tests/Hyperion.Tests.csproj @@ -18,17 +18,54 @@ $(DefineConstants);NETFX - - - true - + + ./lib + true + + + ./lib + true + + + ./lib + true + + + ./lib + true + + + ./lib + true + + + ./lib + true + + + ./lib + true + + + ./lib + true + + + ./lib + true + + + + + + diff --git a/src/Hyperion.Tests/UnsafeDeserializationExclusionTests.cs b/src/Hyperion.Tests/UnsafeDeserializationExclusionTests.cs index e69a58bd..955635e3 100644 --- a/src/Hyperion.Tests/UnsafeDeserializationExclusionTests.cs +++ b/src/Hyperion.Tests/UnsafeDeserializationExclusionTests.cs @@ -1,27 +1,117 @@ using System; +using System.Collections.Generic; using System.IO; -using Hyperion.Extensions; +using System.Runtime.InteropServices; using Hyperion.Internal; using Xunit; using FluentAssertions; +using Hyperion.Extensions; +using Xunit.Abstractions; namespace Hyperion.Tests { public class UnsafeDeserializationExclusionTests { + private readonly ITestOutputHelper _output; + + public UnsafeDeserializationExclusionTests(ITestOutputHelper output) + { + _output = output; + } + [Fact] public void CantDeserializeANaughtyType() { - //System.Diagnostics.Process p = new Process(); - var serializer = new Hyperion.Serializer(); - var di =new System.IO.DirectoryInfo(@"c:\"); + var serializer = new Serializer(SerializerOptions.Default.WithDisallowUnsafeType(false)); + var deserializer = new Serializer(); + var di = new DirectoryInfo(@"c:\"); using (var stream = new MemoryStream()) { serializer.Serialize(di, stream); stream.Position = 0; Assert.Throws(() => - serializer.Deserialize(stream)); + deserializer.Deserialize(stream)); + } + } + + [Fact] + public void CantSerializeANaughtyType() + { + var serializer = new Serializer(); + var di = new FileInfo(@"c:\windows\windows32\dangerous.exe"); + + using (var stream = new MemoryStream()) + { + Assert.Throws(() => + serializer.Serialize(di, stream)); + } + } + + [Theory] + [MemberData(nameof(DangerousObjectFactory))] + public void DetectNaughtyTypesByDefault(Type dangerousType) + { + _output.WriteLine($"Testing for dangerous type [{dangerousType.AssemblyQualifiedName}]"); + TypeEx.IsDisallowedType(dangerousType).Should().BeTrue(); + } +/* + X "System.Security.Principal.WindowsIdentity", + X "System.Security.Principal.WindowsPrincipal", + X "System.Security.Claims.ClaimsIdentity", + X "System.Web.Security.RolePrincipal", + X "System.Windows.Forms.AxHost.State", + X "System.Windows.Data.ObjectDataProvider", + X "System.Management.Automation.PSObject", + X "System.IO.FileSystemInfo", + X "System.IO.FileInfo", + X "System.IdentityModel.Tokens.SessionSecurityToken", + X "SessionViewStateHistoryItem", + X "TextFormattingRunProperties", + X "ToolboxItemContainer", + X "System.CodeDom.Compiler.TempFileCollection", + X "System.Activities.Presentation.WorkflowDesigner", + X "System.Windows.ResourceDictionary", + X "System.Windows.Forms.BindingSource", + X "System.Diagnostics.Process", + "System.Management.IWbemClassObjectFreeThreaded" // Need to have sharepoint installed, simulated + "Microsoft.Exchange.Management.SystemManager.WinForms.ExchangeSettingsProvider", // Need to have ?Exchange? installed, simulated + ??? "System.Security.Principal.WindowsClaimsIdentity", // This FQCN seemed to not exist in the past + */ + public static IEnumerable DangerousObjectFactory() + { + var isWindow = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); + + yield return new object[]{ typeof(System.IO.FileInfo) }; + yield return new object[]{ typeof(System.IO.FileSystemInfo) }; + yield return new object[]{ typeof(System.Security.Claims.ClaimsIdentity)}; + yield return new object[]{ typeof(System.Diagnostics.Process)}; + yield return new object[]{ typeof(System.CodeDom.Compiler.TempFileCollection)}; + yield return new object[]{ typeof(System.Management.IWbemClassObjectFreeThreaded)}; // SIMULATED + yield return new object[]{ typeof(Microsoft.Exchange.Management.SystemManager.WinForms.ExchangeSettingsProvider)}; // SIMULATED +#if !NETFX + yield return new object[]{ typeof(System.Management.Automation.PSObject)}; +#endif + + if (isWindow) + { + yield return new object[]{ typeof(System.Security.Principal.WindowsIdentity) }; + yield return new object[]{ typeof(System.Security.Principal.WindowsPrincipal)}; +#if NETFX + var ass = typeof(System.Web.Mobile.MobileCapabilities).Assembly; + var type = ass.GetType("System.Web.UI.MobileControls.SessionViewState+SessionViewStateHistoryItem"); + yield return new object[]{ type }; + + yield return new object[]{ typeof(System.Drawing.Design.ToolboxItemContainer)}; + yield return new object[]{ typeof(System.Activities.Presentation.WorkflowDesigner)}; + yield return new object[]{ typeof(Microsoft.VisualStudio.Text.Formatting.TextFormattingRunProperties)}; + yield return new object[]{ typeof(System.IdentityModel.Tokens.SessionSecurityToken)}; + yield return new object[]{ typeof(System.Web.Security.RolePrincipal) }; + yield return new object[]{ typeof(System.Windows.Forms.AxHost.State)}; + yield return new object[]{ typeof(System.Windows.Data.ObjectDataProvider)}; + yield return new object[]{ typeof(System.Windows.ResourceDictionary)}; + yield return new object[]{ typeof(System.Windows.Forms.BindingSource)}; +#endif } } @@ -45,17 +135,18 @@ public void TypeFilterShouldThrowOnNaughtyType() var options = SerializerOptions.Default .WithTypeFilter(typeFilter); - var serializer = new Serializer(options); + var serializer = new Serializer(SerializerOptions.Default.WithDisallowUnsafeType(false)); + var deserializer = new Serializer(options); using (var stream = new MemoryStream()) { serializer.Serialize(new ClassA(), stream); stream.Position = 0; - Action act = () => serializer.Deserialize(stream); + Action act = () => deserializer.Deserialize(stream); act.Should().NotThrow(); stream.Position = 0; - Action actObj = () => serializer.Deserialize(stream); + Action actObj = () => deserializer.Deserialize(stream); actObj.Should().NotThrow(); } @@ -63,11 +154,11 @@ public void TypeFilterShouldThrowOnNaughtyType() { serializer.Serialize(new ClassB(), stream); stream.Position = 0; - Action act = () => serializer.Deserialize(stream); + Action act = () => deserializer.Deserialize(stream); act.Should().NotThrow(); stream.Position = 0; - Action actObj = () => serializer.Deserialize(stream); + Action actObj = () => deserializer.Deserialize(stream); actObj.Should().NotThrow(); } @@ -75,13 +166,43 @@ public void TypeFilterShouldThrowOnNaughtyType() { serializer.Serialize(new ClassC(), stream); stream.Position = 0; - Action act = () => serializer.Deserialize(stream); + Action act = () => deserializer.Deserialize(stream); act.Should().Throw(); stream.Position = 0; - Action actObj = () => serializer.Deserialize(stream); + Action actObj = () => deserializer.Deserialize(stream); actObj.Should().Throw(); } } + + [Fact] + public void TypeCacheShouldNotBleedBetweenInstances() + { + var serializer = new Serializer(); + using (var stream = new MemoryStream()) + { + serializer.Serialize(new ClassA(), stream); + stream.Position = 0; + serializer.Deserialize(stream); + } + + // Type should be cached when a serializer deserialize a message + serializer.TypeNameLookup.Values.Should().Contain(typeof(ClassA)); + + // Type cache should not be carried to other serializer instances + var newSerializer = new Serializer(); + newSerializer.TypeNameLookup.Values.Should().NotContain(typeof(ClassA)); + } + } +} + +namespace System.Management +{ + public interface IWbemClassObjectFreeThreaded{ } +} + +namespace Microsoft.Exchange.Management.SystemManager.WinForms +{ + public class ExchangeSettingsProvider { } } \ No newline at end of file diff --git a/src/Hyperion.Tests/lib/Microsoft.VisualStudio.Text.UI.Wpf.dll b/src/Hyperion.Tests/lib/Microsoft.VisualStudio.Text.UI.Wpf.dll new file mode 100644 index 00000000..15e7b226 Binary files /dev/null and b/src/Hyperion.Tests/lib/Microsoft.VisualStudio.Text.UI.Wpf.dll differ diff --git a/src/Hyperion.Tests/lib/PresentationFramework.dll b/src/Hyperion.Tests/lib/PresentationFramework.dll new file mode 100644 index 00000000..12a583e1 Binary files /dev/null and b/src/Hyperion.Tests/lib/PresentationFramework.dll differ diff --git a/src/Hyperion.Tests/lib/System.Activities.Presentation.dll b/src/Hyperion.Tests/lib/System.Activities.Presentation.dll new file mode 100644 index 00000000..7b3b6d47 Binary files /dev/null and b/src/Hyperion.Tests/lib/System.Activities.Presentation.dll differ diff --git a/src/Hyperion.Tests/lib/System.Drawing.Design.dll b/src/Hyperion.Tests/lib/System.Drawing.Design.dll new file mode 100644 index 00000000..b75f1776 Binary files /dev/null and b/src/Hyperion.Tests/lib/System.Drawing.Design.dll differ diff --git a/src/Hyperion.Tests/lib/System.Drawing.dll b/src/Hyperion.Tests/lib/System.Drawing.dll new file mode 100644 index 00000000..af29772c Binary files /dev/null and b/src/Hyperion.Tests/lib/System.Drawing.dll differ diff --git a/src/Hyperion.Tests/lib/System.IdentityModel.dll b/src/Hyperion.Tests/lib/System.IdentityModel.dll new file mode 100644 index 00000000..1d4f60ff Binary files /dev/null and b/src/Hyperion.Tests/lib/System.IdentityModel.dll differ diff --git a/src/Hyperion.Tests/lib/System.Web.Mobile.dll b/src/Hyperion.Tests/lib/System.Web.Mobile.dll new file mode 100644 index 00000000..6091823e Binary files /dev/null and b/src/Hyperion.Tests/lib/System.Web.Mobile.dll differ diff --git a/src/Hyperion.Tests/lib/System.Web.dll b/src/Hyperion.Tests/lib/System.Web.dll new file mode 100644 index 00000000..bb6d2e03 Binary files /dev/null and b/src/Hyperion.Tests/lib/System.Web.dll differ diff --git a/src/Hyperion.Tests/lib/System.Windows.Forms.dll b/src/Hyperion.Tests/lib/System.Windows.Forms.dll new file mode 100644 index 00000000..8df519cc Binary files /dev/null and b/src/Hyperion.Tests/lib/System.Windows.Forms.dll differ diff --git a/src/Hyperion/Extensions/StreamEx.cs b/src/Hyperion/Extensions/StreamEx.cs index 4269930a..e108e4ac 100644 --- a/src/Hyperion/Extensions/StreamEx.cs +++ b/src/Hyperion/Extensions/StreamEx.cs @@ -172,6 +172,26 @@ public static object ReadObject(this Stream stream, DeserializerSession session) return value; } + internal static ByteArrayKey? ReadByteArrayKey(this Stream stream, DeserializerSession session) + { + var length = stream.ReadByte(); + switch (length) + { + case 0: + return null; + case 255: + length = stream.ReadInt32(session); + break; + default: + length--; + break; + } + + var buffer = new byte[length]; + stream.ReadFull(buffer, 0, length); + return new ByteArrayKey(buffer); + } + public static string ReadString(this Stream stream, DeserializerSession session) { var length = stream.ReadByte(); diff --git a/src/Hyperion/Extensions/TypeEx.cs b/src/Hyperion/Extensions/TypeEx.cs index f323b133..35c9a7ae 100644 --- a/src/Hyperion/Extensions/TypeEx.cs +++ b/src/Hyperion/Extensions/TypeEx.cs @@ -14,6 +14,7 @@ using System.IO; using System.Linq; using System.Reflection; +using System.Runtime.CompilerServices; using System.Text.RegularExpressions; using Hyperion.Internal; @@ -51,11 +52,12 @@ internal static class TypeEx { "System.Security.Claims.ClaimsIdentity", "System.Windows.Forms.AxHost.State", + "System.Windows.Forms.AxHost+State", "System.Windows.Data.ObjectDataProvider", "System.Management.Automation.PSObject", "System.Web.Security.RolePrincipal", "System.IdentityModel.Tokens.SessionSecurityToken", - "SessionViewStateHistoryItem", + "System.Web.UI.MobileControls.SessionViewState+SessionViewStateHistoryItem", "TextFormattingRunProperties", "ToolboxItemContainer", "System.Security.Principal.WindowsClaimsIdentity", @@ -63,12 +65,14 @@ internal static class TypeEx "System.Security.Principal.WindowsPrincipal", "System.CodeDom.Compiler.TempFileCollection", "System.IO.FileSystemInfo", + "System.IO.FileInfo", "System.Activities.Presentation.WorkflowDesigner", "System.Windows.ResourceDictionary", "System.Windows.Forms.BindingSource", "Microsoft.Exchange.Management.SystemManager.WinForms.ExchangeSettingsProvider", "System.Diagnostics.Process", - "System.Management.IWbemClassObjectFreeThreaded" + "System.Management.IWbemClassObjectFreeThreaded", + "System.Configuration.Install.AssemblyInstaller" }); public static bool IsHyperionPrimitive(this Type type) @@ -129,9 +133,6 @@ public static bool IsOneDimensionalPrimitiveArray(this Type type) return type.IsArray && type.GetArrayRank() == 1 && type.GetElementType().IsHyperionPrimitive(); } - private static readonly ConcurrentDictionary TypeNameLookup = - new ConcurrentDictionary(ByteArrayKeyComparer.Instance); - public static byte[] GetTypeManifest(IReadOnlyCollection fieldNames) { IEnumerable result = new[] { (byte)fieldNames.Count }; @@ -149,7 +150,8 @@ private static Type GetTypeFromManifestName(Stream stream, DeserializerSession s { var bytes = stream.ReadLengthEncodedByteArray(session); var byteArr = ByteArrayKey.Create(bytes); - return TypeNameLookup.GetOrAdd(byteArr, b => + + return session.Serializer.TypeNameLookup.GetOrAdd(byteArr, b => { var shortName = StringEx.FromUtf8Bytes(b.Bytes, 0, b.Bytes.Length); var overrides = session.Serializer.Options.CrossFrameworkPackageNameOverrides; @@ -167,7 +169,8 @@ private static Type GetTypeFromManifestName(Stream stream, DeserializerSession s }); } - private static bool UnsafeInheritanceCheck(Type type) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static bool UnsafeInheritanceCheck(Type type) { #if NETSTANDARD1_6 if (type.IsValueType()) @@ -181,7 +184,7 @@ private static bool UnsafeInheritanceCheck(Type type) while (currentBase != null) { - if (UnsafeTypesDenySet.Any(r => currentBase.FullName?.Contains(r) ?? false)) + if (IsDisallowedType(currentBase)) return true; #if NETSTANDARD1_6 currentBase = currentBase.DeclaringType; @@ -192,12 +195,25 @@ private static bool UnsafeInheritanceCheck(Type type) return false; } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsDisallowedType() + => IsDisallowedType(typeof(TType)); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsDisallowedType(Type type) + => IsDisallowedType(type.AssemblyQualifiedName); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsDisallowedType(string name) + => UnsafeTypesDenySet.Any(name.Contains); + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Type LoadTypeByName(string name, bool disallowUnsafeTypes, ITypeFilter typeFilter) { if (disallowUnsafeTypes) { - if(UnsafeTypesDenySet.Any(name.Contains)) + if(IsDisallowedType(name)) throw new EvilDeserializationException("Unsafe Type Deserialization Detected!", name); if(!typeFilter.IsAllowed(name)) throw new UserEvilDeserializationException("Unsafe Type Deserialization Detected!", name); @@ -208,7 +224,7 @@ public static Type LoadTypeByName(string name, bool disallowUnsafeTypes, ITypeFi // i.e. if there are different version available in GAC and locally var typename = ToQualifiedAssemblyName(name, ignoreAssemblyVersion: false); var type = Type.GetType(typename, true); - if (UnsafeInheritanceCheck(type)) + if (disallowUnsafeTypes && UnsafeInheritanceCheck(type)) throw new EvilDeserializationException( "Unsafe Type Deserialization Detected!", name); return type; @@ -216,8 +232,8 @@ public static Type LoadTypeByName(string name, bool disallowUnsafeTypes, ITypeFi catch (IOException) { var typename = ToQualifiedAssemblyName(name, ignoreAssemblyVersion: true); - var type = Type.GetType(typename, true); - if (UnsafeInheritanceCheck(type)) + var type = Type.GetType(typename, true); + if (disallowUnsafeTypes && UnsafeInheritanceCheck(type)) throw new EvilDeserializationException( "Unsafe Type Deserialization Detected!", name); return type; diff --git a/src/Hyperion/Serializer.cs b/src/Hyperion/Serializer.cs index 7bae7895..966b5b53 100644 --- a/src/Hyperion/Serializer.cs +++ b/src/Hyperion/Serializer.cs @@ -34,6 +34,12 @@ public class Serializer public readonly SerializerOptions Options; + internal readonly ConcurrentDictionary TypeNameLookup = + new ConcurrentDictionary(ByteArrayKeyComparer.Instance); + + internal readonly ConcurrentDictionary AcceptedTypes = + new ConcurrentDictionary(); + public Serializer() : this(new SerializerOptions()) { } @@ -143,24 +149,45 @@ public void Serialize(object obj, [NotNull] Stream stream, SerializerSession ses { if (obj == null) throw new ArgumentNullException(nameof(obj)); + if(session == null) + throw new ArgumentNullException(nameof(session)); var type = obj.GetType(); + if (Options.DisallowUnsafeTypes) + { + if (AcceptedTypes.TryGetValue(type, out var acceptance)) + { + if(!acceptance.Accepted) + { + if (acceptance.UserRejected) + throw new UserEvilDeserializationException("Unsafe Type Deserialization Detected!", type.FullName); + throw new EvilDeserializationException("Unsafe Type Deserialization Detected!", type.FullName); + } + } + else + { + if(TypeEx.IsDisallowedType(type)) + { + AcceptedTypes[type] = new TypeAccepted(false, false); + throw new EvilDeserializationException("Unsafe Type Deserialization Detected!", type.FullName); + } + if(!Options.TypeFilter.IsAllowed(type.AssemblyQualifiedName)) + { + AcceptedTypes[type] = new TypeAccepted(false, true); + throw new UserEvilDeserializationException("Unsafe Type Deserialization Detected!", + type.FullName); + } + AcceptedTypes[type] = new TypeAccepted(true, false); + } + } + var s = GetSerializerByType(type); s.WriteManifest(stream, session); s.WriteValue(stream, obj, session); } public void Serialize(object obj, [NotNull] Stream stream) - { - if (obj == null) - throw new ArgumentNullException(nameof(obj)); - SerializerSession session = GetSerializerSession(); - - var type = obj.GetType(); - var s = GetSerializerByType(type); - s.WriteManifest(stream, session); - s.WriteValue(stream, obj, session); - } + => Serialize(obj, stream, GetSerializerSession()); public SerializerSession GetSerializerSession() { @@ -297,5 +324,17 @@ public ValueSerializer GetDeserializerByManifest([NotNull] Stream stream, [NotNu throw new NotSupportedException("Unknown manifest value"); } } + + internal readonly struct TypeAccepted + { + public TypeAccepted(bool accepted, bool userRejected) + { + Accepted = accepted; + UserRejected = userRejected; + } + + public bool Accepted { get; } + public bool UserRejected { get; } + } } } \ No newline at end of file diff --git a/src/Hyperion/ValueSerializers/TypeSerializer.cs b/src/Hyperion/ValueSerializers/TypeSerializer.cs index cbf24857..67625e08 100644 --- a/src/Hyperion/ValueSerializers/TypeSerializer.cs +++ b/src/Hyperion/ValueSerializers/TypeSerializer.cs @@ -65,11 +65,14 @@ public override void WriteValue(Stream stream, object value, SerializerSession s new ConcurrentDictionary(); public override object ReadValue(Stream stream, DeserializerSession session) { - var shortname = stream.ReadString(session); - if (shortname == null) + var bytes = stream.ReadByteArrayKey(session); + if (bytes == null) return null; + var byteArr = bytes.Value; + var shortname = StringEx.FromUtf8Bytes(byteArr.Bytes, 0, byteArr.Bytes.Length); var options = session.Serializer.Options; + var type = TypeNameLookup.GetOrAdd(shortname, name => TypeEx.LoadTypeByName(shortname, options.DisallowUnsafeTypes, options.TypeFilter)); diff --git a/src/common.props b/src/common.props index 1896206b..8e24fdac 100644 --- a/src/common.props +++ b/src/common.props @@ -21,11 +21,11 @@ Please report any serialization problem that occurs after an upgrade to this ver net5.0 net471 netstandard2.0 - 1.4.31 - 6.3.0 + 1.4.35 + 6.5.1 2.4.1 2.4.3 - 17.0.0 + 17.1.0 1.2.2 \ No newline at end of file