From 5e581b302c1c18f8e09f069e349f190f64b233b9 Mon Sep 17 00:00:00 2001 From: Anatoli Beliaev Date: Thu, 30 Apr 2026 18:41:36 -0700 Subject: [PATCH] Add HistoryEventSerializationBinder to restrict deserialization Resolves https://msazure.visualstudio.com/Antares/_workitems/edit/37181655 --- .../HistoryEventSerializationBinderTests.cs | 87 +++++++++++++++++++ .../HistoryEventSerializationBinder.cs | 59 +++++++++++++ .../LocalOrchestrationService.cs | 6 +- 3 files changed, 151 insertions(+), 1 deletion(-) create mode 100644 Test/DurableTask.Emulator.Tests/HistoryEventSerializationBinderTests.cs create mode 100644 src/DurableTask.Emulator/HistoryEventSerializationBinder.cs diff --git a/Test/DurableTask.Emulator.Tests/HistoryEventSerializationBinderTests.cs b/Test/DurableTask.Emulator.Tests/HistoryEventSerializationBinderTests.cs new file mode 100644 index 000000000..8b85a2712 --- /dev/null +++ b/Test/DurableTask.Emulator.Tests/HistoryEventSerializationBinderTests.cs @@ -0,0 +1,87 @@ +// ---------------------------------------------------------------------------------- +// Copyright Microsoft Corporation +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +namespace DurableTask.Emulator.Tests +{ + using System; + using System.Collections.Generic; + using DurableTask.Core; + using DurableTask.Core.History; + using Microsoft.VisualStudio.TestTools.UnitTesting; + using Newtonsoft.Json; + + [TestClass] + public class HistoryEventSerializationBinderTests + { + // Mirrors the production state settings used by LocalOrchestrationService. + static readonly JsonSerializerSettings StateJsonSettings = new JsonSerializerSettings + { + TypeNameHandling = TypeNameHandling.Auto, + SerializationBinder = new HistoryEventSerializationBinder() + }; + + [TestMethod] + public void RoundTripsHistoryEventListWithPolymorphicEventsAndTags() + { + IList original = new List + { + new ExecutionStartedEvent(eventId: -1, input: "input") + { + Name = "Orchestration", + Version = "1.0", + OrchestrationInstance = new OrchestrationInstance + { + InstanceId = "instance-1", + ExecutionId = "execution-1" + }, + Tags = new Dictionary { ["tag1"] = "value1" } + }, + new TaskScheduledEvent(eventId: 1) + { + Name = "Activity", + Version = "1.0", + Input = "activity-input" + } + }; + + string json = JsonConvert.SerializeObject(original, StateJsonSettings); + var deserialized = JsonConvert.DeserializeObject>(json, StateJsonSettings); + + Assert.AreEqual(2, deserialized.Count); + Assert.IsInstanceOfType(deserialized[0], typeof(ExecutionStartedEvent)); + var startedEvent = (ExecutionStartedEvent)deserialized[0]; + Assert.AreEqual("Orchestration", startedEvent.Name); + Assert.AreEqual("input", startedEvent.Input); + Assert.AreEqual("value1", startedEvent.Tags["tag1"]); + Assert.IsInstanceOfType(deserialized[1], typeof(TaskScheduledEvent)); + Assert.AreEqual("Activity", ((TaskScheduledEvent)deserialized[1]).Name); + } + + [TestMethod] + public void RejectsNonAllowlistedRootType() + { + string json = "{\"$type\":\"System.IO.FileInfo, System.Private.CoreLib\",\"OriginalPath\":\"c:\\\\evil\"}"; + Assert.ThrowsException( + () => JsonConvert.DeserializeObject(json, StateJsonSettings)); + } + + [TestMethod] + public void RejectsNonAllowlistedNestedType() + { + string json = "[{\"$type\":\"DurableTask.Core.History.ExecutionStartedEvent, DurableTask.Core\"," + + "\"Tags\":{\"$type\":\"System.Collections.Generic.SortedDictionary`2[[System.String, System.Private.CoreLib],[System.String, System.Private.CoreLib]], System.Collections\"}}]"; + Assert.ThrowsException( + () => JsonConvert.DeserializeObject>(json, StateJsonSettings)); + } + } +} diff --git a/src/DurableTask.Emulator/HistoryEventSerializationBinder.cs b/src/DurableTask.Emulator/HistoryEventSerializationBinder.cs new file mode 100644 index 000000000..45df789ac --- /dev/null +++ b/src/DurableTask.Emulator/HistoryEventSerializationBinder.cs @@ -0,0 +1,59 @@ +// ---------------------------------------------------------------------------------- +// Copyright Microsoft Corporation +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +namespace DurableTask.Emulator +{ + using System; + using System.Collections.Generic; + using System.Reflection; + using DurableTask.Core.History; + using DurableTask.Core.Serializing; + using Newtonsoft.Json; + + /// + /// Strict used by the in-memory + /// emulator to round-trip the of that + /// represents an orchestration's runtime state. Only types defined in DurableTask.Core, + /// plus of strings, are accepted; any other $type + /// token is rejected with a . + /// + public sealed class HistoryEventSerializationBinder : PackageUpgradeSerializationBinder + { + static readonly Assembly DurableTaskCoreAssembly = typeof(HistoryEvent).Assembly; + + /// + public override Type BindToType(string assemblyName, string typeName) + { + Type resolved = base.BindToType(assemblyName, typeName); + + if (resolved == null || !IsAllowed(resolved)) + { + throw new JsonSerializationException( + $"Type '{typeName}' from assembly '{assemblyName}' is not permitted by the orchestration runtime state serialization binder."); + } + + return resolved; + } + + static bool IsAllowed(Type type) + { + // Allow types defined in DurableTask.Core (HistoryEvent subclasses, OrchestrationInstance, + // ParentInstance, FailureDetails, etc.), plus Dictionary for the + // IDictionary Tags members declared on ExecutionStartedEvent / + // SubOrchestrationInstanceCreatedEvent / TaskScheduledEvent (Newtonsoft.Json emits a + // $type for these because the static declared type is an interface). + return type.Assembly == DurableTaskCoreAssembly + || type == typeof(Dictionary); + } + } +} diff --git a/src/DurableTask.Emulator/LocalOrchestrationService.cs b/src/DurableTask.Emulator/LocalOrchestrationService.cs index 50e43ecd5..69ac3ec6a 100644 --- a/src/DurableTask.Emulator/LocalOrchestrationService.cs +++ b/src/DurableTask.Emulator/LocalOrchestrationService.cs @@ -54,7 +54,11 @@ public class LocalOrchestrationService : IOrchestrationService, IOrchestrationSe readonly object timerLock = new object(); readonly ConcurrentDictionary> orchestrationWaiters; - static readonly JsonSerializerSettings StateJsonSettings = new JsonSerializerSettings { TypeNameHandling = TypeNameHandling.Auto }; + static readonly JsonSerializerSettings StateJsonSettings = new JsonSerializerSettings + { + TypeNameHandling = TypeNameHandling.Auto, + SerializationBinder = new HistoryEventSerializationBinder() + }; /// /// Creates a new instance of the LocalOrchestrationService with default settings