diff --git a/Test/DurableTask.AzureServiceFabric.Tests/RemoteOrchestrationTypeBinderTests.cs b/Test/DurableTask.AzureServiceFabric.Tests/RemoteOrchestrationTypeBinderTests.cs new file mode 100644 index 000000000..89e62bf00 --- /dev/null +++ b/Test/DurableTask.AzureServiceFabric.Tests/RemoteOrchestrationTypeBinderTests.cs @@ -0,0 +1,100 @@ +// ---------------------------------------------------------------------------------- +// 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.AzureServiceFabric.Tests +{ + using System; + using System.Collections.Generic; + using DurableTask.AzureServiceFabric.Remote; + using DurableTask.Core; + using DurableTask.Core.History; + using Microsoft.VisualStudio.TestTools.UnitTesting; + using Newtonsoft.Json; + + [TestClass] + public class RemoteOrchestrationTypeBinderTests + { + // Mirrors the production formatter settings configured in RemoteOrchestrationServiceClient.PutJsonAsync. + static readonly JsonSerializerSettings RoundTripSettings = new JsonSerializerSettings + { + TypeNameHandling = TypeNameHandling.Auto, + SerializationBinder = new RemoteOrchestrationTypeBinder() + }; + + [TestMethod] + public void RoundTripsTaskMessageWithPolymorphicHistoryEvent() + { + var original = new TaskMessage + { + SequenceNumber = 7, + OrchestrationInstance = new OrchestrationInstance + { + InstanceId = "instance-1", + ExecutionId = "execution-1" + }, + Event = new ExecutionStartedEvent(eventId: -1, input: "input") + { + Name = "Orchestration", + Version = "1.0", + Tags = new Dictionary { ["tag1"] = "value1" } + } + }; + + string json = JsonConvert.SerializeObject(original, RoundTripSettings); + var deserialized = JsonConvert.DeserializeObject(json, RoundTripSettings); + + Assert.IsInstanceOfType(deserialized.Event, typeof(ExecutionStartedEvent)); + var startedEvent = (ExecutionStartedEvent)deserialized.Event; + Assert.AreEqual("Orchestration", startedEvent.Name); + Assert.AreEqual("input", startedEvent.Input); + Assert.AreEqual("value1", startedEvent.Tags["tag1"]); + } + + [TestMethod] + public void AllowsTypesFromDurableTaskCoreAssembly() + { + var binder = new RemoteOrchestrationTypeBinder(); + Type bound = binder.BindToType( + typeof(TaskMessage).Assembly.GetName().Name, + typeof(TaskMessage).FullName); + Assert.AreEqual(typeof(TaskMessage), bound); + } + + [TestMethod] + public void AllowsTypesFromDurableTaskAzureServiceFabricAssembly() + { + var binder = new RemoteOrchestrationTypeBinder(); + Type bound = binder.BindToType( + typeof(TaskMessageItem).Assembly.GetName().Name, + typeof(TaskMessageItem).FullName); + Assert.AreEqual(typeof(TaskMessageItem), bound); + } + + [TestMethod] + public void RejectsNonAllowlistedRootType() + { + string json = "{\"$type\":\"System.IO.FileInfo, System.Private.CoreLib\",\"OriginalPath\":\"c:\\\\evil\"}"; + Assert.ThrowsException( + () => JsonConvert.DeserializeObject(json, RoundTripSettings)); + } + + [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, RoundTripSettings)); + } + } +} diff --git a/src/DurableTask.AzureServiceFabric/Remote/RemoteOrchestrationServiceClient.cs b/src/DurableTask.AzureServiceFabric/Remote/RemoteOrchestrationServiceClient.cs index 2143dd414..e43db2ab6 100644 --- a/src/DurableTask.AzureServiceFabric/Remote/RemoteOrchestrationServiceClient.cs +++ b/src/DurableTask.AzureServiceFabric/Remote/RemoteOrchestrationServiceClient.cs @@ -305,7 +305,15 @@ private async Task PutJsonAsync(string instanceId, string fragment, object @obje { var mediaFormatter = new JsonMediaTypeFormatter() { - SerializerSettings = new JsonSerializerSettings { TypeNameHandling = TypeNameHandling.All } + // TypeNameHandling.Auto is sufficient: it emits $type only for polymorphic members + // (e.g., HistoryEvent Event on TaskMessage). The strict binder restricts the resolvable + // type set to DurableTask.* assemblies as defense in depth in case this formatter is + // ever reused for deserialization. + SerializerSettings = new JsonSerializerSettings + { + TypeNameHandling = TypeNameHandling.Auto, + SerializationBinder = new RemoteOrchestrationTypeBinder(), + } }; using (var result = await this.ExecuteRequestWithRetriesAsync( diff --git a/src/DurableTask.AzureServiceFabric/Remote/RemoteOrchestrationTypeBinder.cs b/src/DurableTask.AzureServiceFabric/Remote/RemoteOrchestrationTypeBinder.cs new file mode 100644 index 000000000..d141077da --- /dev/null +++ b/src/DurableTask.AzureServiceFabric/Remote/RemoteOrchestrationTypeBinder.cs @@ -0,0 +1,66 @@ +// ---------------------------------------------------------------------------------- +// 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.AzureServiceFabric.Remote +{ + using System; + using System.Collections.Generic; + using System.Reflection; + using DurableTask.Core; + using DurableTask.Core.Serializing; + using Newtonsoft.Json; + + /// + /// Strict used by + /// when configuring the JSON formatter that + /// serializes orchestration RPC payloads (TaskMessage, CreateTaskOrchestrationParameters, + /// etc.). Only types defined in DurableTask.Core and DurableTask.AzureServiceFabric, + /// plus of strings, are accepted; any other $type + /// token is rejected with a . + /// + /// + /// The formatter that uses this binder is only consumed for outbound serialization in + /// ; the binder is provided as defense in depth so + /// that the same settings remain safe if reused for deserialization. + /// + internal sealed class RemoteOrchestrationTypeBinder : PackageUpgradeSerializationBinder + { + static readonly Assembly DurableTaskCoreAssembly = typeof(TaskMessage).Assembly; + static readonly Assembly DurableTaskAzureServiceFabricAssembly = typeof(RemoteOrchestrationTypeBinder).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 remote orchestration serialization binder."); + } + + return resolved; + } + + static bool IsAllowed(Type type) + { + // Allow types defined in DurableTask.Core (TaskMessage, HistoryEvent subclasses, + // OrchestrationInstance, etc.) and DurableTask.AzureServiceFabric (CreateTaskOrchestrationParameters, + // PurgeOrchestrationHistoryParameters, etc.), plus Dictionary for the + // IDictionary Tags members on history events. + return type.Assembly == DurableTaskCoreAssembly + || type.Assembly == DurableTaskAzureServiceFabricAssembly + || type == typeof(Dictionary); + } + } +}