From b02755a378c9cb6cd0ccdfeef18c7d3cd5b6747c Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Thu, 2 Mar 2023 21:19:23 +0100 Subject: [PATCH] Prevent stack overflow when evaluating input arguments --- .../Features/JavaScriptFeature.cs | 13 +++- ...flowDefinitionActivityJavaScriptHandler.cs | 73 +++++++++++++++++++ .../Services/JintJavaScriptEvaluator.cs | 25 ------- 3 files changed, 84 insertions(+), 27 deletions(-) create mode 100644 src/modules/Elsa.JavaScript/Handlers/WorkflowDefinitionActivityJavaScriptHandler.cs diff --git a/src/modules/Elsa.JavaScript/Features/JavaScriptFeature.cs b/src/modules/Elsa.JavaScript/Features/JavaScriptFeature.cs index d2ff8d3d1..6f25bff29 100644 --- a/src/modules/Elsa.JavaScript/Features/JavaScriptFeature.cs +++ b/src/modules/Elsa.JavaScript/Features/JavaScriptFeature.cs @@ -33,6 +33,13 @@ public class JavaScriptFeature : FeatureBase /// public override void Configure() { + Module.UseWorkflowManagement(management => management.AddActivitiesFrom()); + } + + /// + public override void Apply() + { + // JavaScript services. Services .AddSingleton() .AddSingleton() @@ -41,6 +48,7 @@ public class JavaScriptFeature : FeatureBase .AddExpressionHandler() ; + // Type definition services. Services .AddSingleton() .AddSingleton() @@ -50,7 +58,8 @@ public class JavaScriptFeature : FeatureBase .AddSingleton() .AddSingleton() ; - - Module.UseWorkflowManagement(management => management.AddActivitiesFrom()); + + // Handlers. + Services.AddNotificationHandlersFrom(); } } \ No newline at end of file diff --git a/src/modules/Elsa.JavaScript/Handlers/WorkflowDefinitionActivityJavaScriptHandler.cs b/src/modules/Elsa.JavaScript/Handlers/WorkflowDefinitionActivityJavaScriptHandler.cs new file mode 100644 index 000000000..281d2a751 --- /dev/null +++ b/src/modules/Elsa.JavaScript/Handlers/WorkflowDefinitionActivityJavaScriptHandler.cs @@ -0,0 +1,73 @@ +using Elsa.Expressions.Models; +using Elsa.Expressions.Services; +using Elsa.Extensions; +using Elsa.JavaScript.Notifications; +using Elsa.Mediator.Services; +using Elsa.Workflows.Core.Models; +using Elsa.Workflows.Management.Activities; +using Elsa.Workflows.Management.Extensions; +using Elsa.Workflows.Management.Services; +using Humanizer; +using JetBrains.Annotations; +using Jint; + +namespace Elsa.JavaScript.Handlers; + +/// +/// Configures the JavaScript engine with workflow input getters. +/// +[PublicAPI] +public class WorkflowDefinitionActivityJavaScriptHandler : INotificationHandler +{ + private readonly IActivityRegistry _activityRegistry; + private readonly IExpressionEvaluator _expressionEvaluator; + + /// + /// Constructor. + /// + public WorkflowDefinitionActivityJavaScriptHandler(IActivityRegistry activityRegistry, IExpressionEvaluator expressionEvaluator) + { + _activityRegistry = activityRegistry; + _expressionEvaluator = expressionEvaluator; + } + + /// + public async Task HandleAsync(EvaluatingJavaScript notification, CancellationToken cancellationToken) + { + var engine = notification.Engine; + var context = notification.Context; + + // If we are already evaluating inputs, then we're in a circular evaluation loop. In this case, we should not attempt to evaluate the inputs. + if(context.TransientProperties.TryGetValue("EvaluatingInputs", out var evaluatingInputs) && (bool)evaluatingInputs) + return; + + // To prevent a circular evaluation loop, set a flag on the context to indicate that we're currently evaluating the inputs. + context.TransientProperties["EvaluatingInputs"] = true; + + // Create input getters. + await CreateInputAccessorsAsync(engine, context); + } + + private async Task CreateInputAccessorsAsync(Engine engine, ExpressionExecutionContext context) + { + var workflowDefinitionActivity = GetFirstWorkflowDefinitionActivity(context); + + if (workflowDefinitionActivity == null) + return; + + var descriptor = _activityRegistry.Find(workflowDefinitionActivity.Type, workflowDefinitionActivity.Version)!; + var inputDefinitions = descriptor.Inputs; + + foreach (var inputDefinition in inputDefinitions) + { + var inputPascalName = inputDefinition.Name.Pascalize(); + var input = workflowDefinitionActivity.SyntheticProperties.TryGetValue(inputDefinition.Name, out var inputValue) ? (Input?)inputValue : default; + var evaluatedExpression = input != null ? await _expressionEvaluator.EvaluateAsync(input, context) : input; + + engine.SetValue($"get{inputPascalName}", (Func)(() => evaluatedExpression)); + } + } + + private static WorkflowDefinitionActivity? GetFirstWorkflowDefinitionActivity(ExpressionExecutionContext context) => + context.GetActivityExecutionContext().GetFirstWorkflowDefinitionActivity(); +} \ No newline at end of file diff --git a/src/modules/Elsa.JavaScript/Services/JintJavaScriptEvaluator.cs b/src/modules/Elsa.JavaScript/Services/JintJavaScriptEvaluator.cs index d1f4b4a66..3b3e41b05 100644 --- a/src/modules/Elsa.JavaScript/Services/JintJavaScriptEvaluator.cs +++ b/src/modules/Elsa.JavaScript/Services/JintJavaScriptEvaluator.cs @@ -74,8 +74,6 @@ public class JintJavaScriptEvaluator : IJavaScriptEvaluator // Create variable & input setters and getters for each variable. CreateMemoryBlockAccessors(engine, context); - // Create input getters. - await CreateInputAccessorsAsync(engine, context); engine.SetValue("isNullOrWhiteSpace", (Func)(value => string.IsNullOrWhiteSpace(value))); engine.SetValue("isNullOrEmpty", (Func)(value => string.IsNullOrEmpty(value))); @@ -93,29 +91,6 @@ public class JintJavaScriptEvaluator : IJavaScriptEvaluator return engine; } - - private async Task CreateInputAccessorsAsync(Engine engine, ExpressionExecutionContext context) - { - var workflowDefinitionActivity = GetFirstWorkflowDefinitionActivity(context); - - if (workflowDefinitionActivity == null) - return; - - var descriptor = _activityRegistry.Find(workflowDefinitionActivity.Type, workflowDefinitionActivity.Version)!; - var inputDefinitions = descriptor.Inputs; - - foreach (var inputDefinition in inputDefinitions) - { - var inputPascalName = inputDefinition.Name.Pascalize(); - var input = workflowDefinitionActivity.SyntheticProperties.TryGetValue(inputDefinition.Name, out var inputValue) ? (Input?)inputValue : default; - var evaluatedExpression = input != null ? await _expressionEvaluator.EvaluateAsync(input, context) : input; - - engine.SetValue($"get{inputPascalName}", (Func)(() => evaluatedExpression)); - } - } - - private static WorkflowDefinitionActivity? GetFirstWorkflowDefinitionActivity(ExpressionExecutionContext context) => - context.GetActivityExecutionContext().GetFirstWorkflowDefinitionActivity(); private static void CreateMemoryBlockAccessors(Engine engine, ExpressionExecutionContext context) {