-
Notifications
You must be signed in to change notification settings - Fork 325
Add HistoryEventSerializationBinder to restrict deserialization #1345
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<HistoryEvent> original = new List<HistoryEvent> | ||
| { | ||
| new ExecutionStartedEvent(eventId: -1, input: "input") | ||
| { | ||
| Name = "Orchestration", | ||
| Version = "1.0", | ||
| OrchestrationInstance = new OrchestrationInstance | ||
| { | ||
| InstanceId = "instance-1", | ||
| ExecutionId = "execution-1" | ||
| }, | ||
| Tags = new Dictionary<string, string> { ["tag1"] = "value1" } | ||
| }, | ||
| new TaskScheduledEvent(eventId: 1) | ||
| { | ||
| Name = "Activity", | ||
| Version = "1.0", | ||
| Input = "activity-input" | ||
| } | ||
| }; | ||
|
|
||
| string json = JsonConvert.SerializeObject(original, StateJsonSettings); | ||
| var deserialized = JsonConvert.DeserializeObject<IList<HistoryEvent>>(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<JsonSerializationException>( | ||
| () => JsonConvert.DeserializeObject<HistoryEvent>(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<JsonSerializationException>( | ||
| () => JsonConvert.DeserializeObject<IList<HistoryEvent>>(json, StateJsonSettings)); | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -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; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// <summary> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// Strict <see cref="Newtonsoft.Json.Serialization.ISerializationBinder"/> used by the in-memory | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// emulator to round-trip the <see cref="IList{T}"/> of <see cref="HistoryEvent"/> that | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// represents an orchestration's runtime state. Only types defined in <c>DurableTask.Core</c>, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// plus <see cref="Dictionary{TKey, TValue}"/> of strings, are accepted; any other <c>$type</c> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// token is rejected with a <see cref="JsonSerializationException"/>. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// </summary> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public sealed class HistoryEventSerializationBinder : PackageUpgradeSerializationBinder | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| static readonly Assembly DurableTaskCoreAssembly = typeof(HistoryEvent).Assembly; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// <inheritdoc /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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<string, string> for the | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // IDictionary<string, string> 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<string, string>); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+51
to
+56
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // ParentInstance, FailureDetails, etc.), plus Dictionary<string, string> for the | |
| // IDictionary<string, string> 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<string, string>); | |
| // ParentInstance, FailureDetails, etc.), plus the constrained collection shapes that | |
| // Json.NET may emit as $type metadata when TypeNameHandling.Auto is enabled: | |
| // * List<T> where T is itself allowed (for IList<HistoryEvent> runtime state) | |
| // * Dictionary<TKey, TValue> where both generic arguments are allowed | |
| // String is permitted as a terminal type so Dictionary<string, string> continues to | |
| // round-trip for the IDictionary<string, string> Tags members declared on | |
| // ExecutionStartedEvent / SubOrchestrationInstanceCreatedEvent / TaskScheduledEvent. | |
| if (type.Assembly == DurableTaskCoreAssembly || type == typeof(string)) | |
| { | |
| return true; | |
| } | |
| if (type.IsGenericType) | |
| { | |
| Type genericTypeDefinition = type.GetGenericTypeDefinition(); | |
| Type[] genericArguments = type.GetGenericArguments(); | |
| if (genericTypeDefinition == typeof(List<>)) | |
| { | |
| return genericArguments.Length == 1 && IsAllowed(genericArguments[0]); | |
| } | |
| if (genericTypeDefinition == typeof(Dictionary<,>)) | |
| { | |
| return genericArguments.Length == 2 | |
| && IsAllowed(genericArguments[0]) | |
| && IsAllowed(genericArguments[1]); | |
| } | |
| } | |
| return false; |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -54,7 +54,11 @@ public class LocalOrchestrationService : IOrchestrationService, IOrchestrationSe | |||||
| readonly object timerLock = new object(); | ||||||
|
|
||||||
| readonly ConcurrentDictionary<string, TaskCompletionSource<OrchestrationState>> orchestrationWaiters; | ||||||
| static readonly JsonSerializerSettings StateJsonSettings = new JsonSerializerSettings { TypeNameHandling = TypeNameHandling.Auto }; | ||||||
| static readonly JsonSerializerSettings StateJsonSettings = new JsonSerializerSettings | ||||||
| { | ||||||
| TypeNameHandling = TypeNameHandling.Auto, | ||||||
|
||||||
| TypeNameHandling = TypeNameHandling.Auto, | |
| TypeNameHandling = TypeNameHandling.Objects, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This test file is under the legacy
Test/directory, but the solution and active test projects referencetest/DurableTask.Emulator.Tests(lowercase). As a result, these tests likely won’t be compiled or executed in CI. Move the test intotest/DurableTask.Emulator.Tests/(or ensure the referencedtest/DurableTask.Emulator.Tests.csprojincludes it).