From 9af706a72488332a41e01c75eb6da6c276485748 Mon Sep 17 00:00:00 2001 From: RoosterDragon Date: Thu, 13 Jul 2023 15:40:38 +0100 Subject: [PATCH] Introduce PartialRecursionDepthLimit When evaluating templates with partials, it is possible to recurse in the evaluation of those partials. This can be useful for dealing with tree like data, such as rendering a list of friends-of-friends-of-friends-of-etc.... The ability to recurse can lead to stack overflows. For example if a sufficiently deep tree is provided as input data, or more simply if the partial calls itself in an infinite loop. As a stack overflow terminates the process, this is not desirable behaviour as it is an unavoidable crash. To resolve this a configurable PartialRecursionDepthLimit is introduced, defaulting to 100. Now when a template is evaluated a HandlebarsRuntimeException will be thrown if this limit is reached. This allows the caller to catch the exception and recover gracefully, rather than terminating the process. --- source/Handlebars.Test/InlinePartialTests.cs | 60 ++++++++++- source/Handlebars.Test/PartialTests.cs | 102 ++++++++++++++++++ source/Handlebars/BindingContext.cs | 2 + .../Translation/Expression/PartialBinder.cs | 21 +++- .../Configuration/HandlebarsConfiguration.cs | 5 + .../HandlebarsConfigurationAdapter.cs | 1 + .../ICompiledHandlebarsConfiguration.cs | 2 + .../Handlebars/Pools/BindingContext.Pool.cs | 1 + 8 files changed, 192 insertions(+), 2 deletions(-) diff --git a/source/Handlebars.Test/InlinePartialTests.cs b/source/Handlebars.Test/InlinePartialTests.cs index 3e6aa4ec..7d4a019f 100644 --- a/source/Handlebars.Test/InlinePartialTests.cs +++ b/source/Handlebars.Test/InlinePartialTests.cs @@ -1,4 +1,5 @@ -using Xunit; +using System.Collections.Generic; +using Xunit; namespace HandlebarsDotNet.Test { @@ -355,6 +356,63 @@ public void InlinePartialInEach() var result = template(data); Assert.Equal("12", result); } + + [Fact] + public void RecursionUnboundedInlinePartial() + { + string source = "{{#*inline \"list\"}}{{>list}}{{/inline}}{{>list}}"; + + var template = Handlebars.Compile(source); + + string Result() => template(null); + var ex = Assert.Throws(Result); + while (ex.InnerException != null) + ex = Assert.IsType(ex.InnerException); + Assert.Equal("Runtime error while rendering partial 'list', exceeded recursion depth limit of 100", ex.Message); + } + + [Fact] + public void RecursionBoundedToLimitInlinePartial() + { + string source = "{{#*inline \"list\"}}x{{#each items}}{{#if items}}{{>list}}{{/if}}{{/each}}{{/inline}}{{>list}}{{>list}}"; + + var template = Handlebars.Compile(source); + + var data = new Dictionary(); + var items = data; + for (var depth = 0; depth < Handlebars.Configuration.PartialRecursionDepthLimit; depth++) + { + var nestedItems = new Dictionary(); + items.Add("items", new[] { nestedItems }); + items = nestedItems; + } + + var result = template(data); + Assert.Equal(new string('x', Handlebars.Configuration.PartialRecursionDepthLimit * 2), result); + } + + [Fact] + public void RecursionBoundedAboveLimitInlinePartial() + { + string source = "{{#*inline \"list\"}}x{{#each items}}{{#if items}}{{>list}}{{/if}}{{/each}}{{/inline}}{{>list}}"; + + var template = Handlebars.Compile(source); + + var data = new Dictionary(); + var items = data; + for (var depth = 0; depth < Handlebars.Configuration.PartialRecursionDepthLimit + 1; depth++) + { + var nestedItems = new Dictionary(); + items.Add("items", new[] { nestedItems }); + items = nestedItems; + } + + string Result() => template(data); + var ex = Assert.Throws(Result); + while (ex.InnerException != null) + ex = Assert.IsType(ex.InnerException); + Assert.Equal("Runtime error while rendering partial 'list', exceeded recursion depth limit of 100", ex.Message); + } } } diff --git a/source/Handlebars.Test/PartialTests.cs b/source/Handlebars.Test/PartialTests.cs index 4a0784db..bbd8e896 100644 --- a/source/Handlebars.Test/PartialTests.cs +++ b/source/Handlebars.Test/PartialTests.cs @@ -660,6 +660,30 @@ public void BlockPartialWithNestedSpecialNamedPartial2() Assert.Equal("A 1 B 3 C 4 D 2 E", result); } + [Fact] + public void RecursionUnboundedBlockPartialWithSpecialNamedPartial() + { + string source = "{{#>myPartial}}{{>myPartial}}{{/myPartial}}"; + + var template = Handlebars.Compile(source); + + var partialSource = "{{> @partial-block }}"; + using (var reader = new StringReader(partialSource)) + { + var partialTemplate = Handlebars.Compile(reader); + Handlebars.RegisterTemplate("myPartial", partialTemplate); + } + + string Result() => template(null); + var ex = Assert.Throws(Result); + Assert.Equal("Runtime error while rendering partial 'myPartial', see inner exception for more information", ex.Message); + ex = Assert.IsType(ex.InnerException); + Assert.Equal("Runtime error while rendering partial 'myPartial', see inner exception for more information", ex.Message); + ex = Assert.IsType(ex.InnerException); + Assert.Equal("Referenced partial name @partial-block could not be resolved", ex.Message); + Assert.Null(ex.InnerException); + } + [Fact] public void TemplateWithSpecialNamedPartial() { @@ -717,6 +741,84 @@ public void SubExpressionPartial() var result = template(data); Assert.Equal("Hello, world!", result); } + + [Fact] + public void RecursionUnboundedPartial() + { + string source = "{{>list}}"; + + var template = Handlebars.Compile(source); + + var partialSource = "{{>list}}"; + using (var reader = new StringReader(partialSource)) + { + var partialTemplate = Handlebars.Compile(reader); + Handlebars.RegisterTemplate("list", partialTemplate); + } + + string Result() => template(null); + var ex = Assert.Throws(Result); + while (ex.InnerException != null) + ex = Assert.IsType(ex.InnerException); + Assert.Equal("Runtime error while rendering partial 'list', exceeded recursion depth limit of 100", ex.Message); + } + + [Fact] + public void RecursionBoundedToLimitPartial() + { + string source = "{{>list}}{{>list}}"; + + var template = Handlebars.Compile(source); + + var partialSource = "x{{#each items}}{{#if items}}{{>list}}{{/if}}{{/each}}"; + using (var reader = new StringReader(partialSource)) + { + var partialTemplate = Handlebars.Compile(reader); + Handlebars.RegisterTemplate("list", partialTemplate); + } + + var data = new Dictionary(); + var items = data; + for (var depth = 0; depth < Handlebars.Configuration.PartialRecursionDepthLimit; depth++) + { + var nestedItems = new Dictionary(); + items.Add("items", new[] { nestedItems }); + items = nestedItems; + } + + var result = template(data); + Assert.Equal(new string('x', Handlebars.Configuration.PartialRecursionDepthLimit * 2), result); + } + + [Fact] + public void RecursionBoundedAboveLimitPartial() + { + string source = "{{>list}}"; + + var template = Handlebars.Compile(source); + + var partialSource = "x{{#each items}}{{#if items}}{{>list}}{{/if}}{{/each}}"; + using (var reader = new StringReader(partialSource)) + { + var partialTemplate = Handlebars.Compile(reader); + Handlebars.RegisterTemplate("list", partialTemplate); + } + + var data = new Dictionary(); + var items = data; + for (var depth = 0; depth < Handlebars.Configuration.PartialRecursionDepthLimit + 1; depth++) + { + var nestedItems = new Dictionary(); + items.Add("items", new[] { nestedItems }); + items = nestedItems; + } + + string Result() => template(data); + var ex = Assert.Throws(Result); + while (ex.InnerException != null) + ex = Assert.IsType(ex.InnerException); + Assert.Equal("Runtime error while rendering partial 'list', exceeded recursion depth limit of 100", ex.Message); + } } } diff --git a/source/Handlebars/BindingContext.cs b/source/Handlebars/BindingContext.cs index 089cf276..2f1e6c24 100644 --- a/source/Handlebars/BindingContext.cs +++ b/source/Handlebars/BindingContext.cs @@ -117,6 +117,8 @@ out WellKnownVariables[(int) WellKnownVariable.Parent] internal TemplateDelegate PartialBlockTemplate { get; set; } + internal short PartialDepth { get; set; } + public object Value { get; set; } [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/source/Handlebars/Compiler/Translation/Expression/PartialBinder.cs b/source/Handlebars/Compiler/Translation/Expression/PartialBinder.cs index 247e9c58..57467832 100644 --- a/source/Handlebars/Compiler/Translation/Expression/PartialBinder.cs +++ b/source/Handlebars/Compiler/Translation/Expression/PartialBinder.cs @@ -133,10 +133,24 @@ private static bool InvokePartial( return true; } + void IncreaseDepth() + { + if (++context.PartialDepth > configuration.PartialRecursionDepthLimit) + throw new HandlebarsRuntimeException($"Runtime error while rendering partial '{partialName}', exceeded recursion depth limit of {configuration.PartialRecursionDepthLimit}"); + } + //if we have an inline partial, skip the file system and RegisteredTemplates collection if (context.InlinePartialTemplates.TryGetValue(partialName, out var partial)) { - partial(writer, context); + IncreaseDepth(); + try + { + partial(writer, context); + } + finally + { + context.PartialDepth--; + } return true; } @@ -152,6 +166,7 @@ private static bool InvokePartial( } } + IncreaseDepth(); try { using var textWriter = writer.CreateWrapper(); @@ -162,6 +177,10 @@ private static bool InvokePartial( { throw new HandlebarsRuntimeException($"Runtime error while rendering partial '{partialName}', see inner exception for more information", exception); } + finally + { + context.PartialDepth--; + } } } } diff --git a/source/Handlebars/Configuration/HandlebarsConfiguration.cs b/source/Handlebars/Configuration/HandlebarsConfiguration.cs index c7d4d09a..9342e067 100644 --- a/source/Handlebars/Configuration/HandlebarsConfiguration.cs +++ b/source/Handlebars/Configuration/HandlebarsConfiguration.cs @@ -59,6 +59,11 @@ public string UnresolvedBindingFormatter /// public IMissingPartialTemplateHandler MissingPartialTemplateHandler { get; set; } + /// + /// Maximum depth to recurse into partial templates when evaluating the template. Defaults to 100. + /// + public short PartialRecursionDepthLimit { get; set; } = 100; + /// public IAppendOnlyList AliasProviders { get; } = new ObservableList(); diff --git a/source/Handlebars/Configuration/HandlebarsConfigurationAdapter.cs b/source/Handlebars/Configuration/HandlebarsConfigurationAdapter.cs index 94f5ff61..d589c4c2 100644 --- a/source/Handlebars/Configuration/HandlebarsConfigurationAdapter.cs +++ b/source/Handlebars/Configuration/HandlebarsConfigurationAdapter.cs @@ -63,6 +63,7 @@ public HandlebarsConfigurationAdapter(HandlebarsConfiguration configuration) public bool ThrowOnUnresolvedBindingExpression => UnderlingConfiguration.ThrowOnUnresolvedBindingExpression; public IPartialTemplateResolver PartialTemplateResolver => UnderlingConfiguration.PartialTemplateResolver; public IMissingPartialTemplateHandler MissingPartialTemplateHandler => UnderlingConfiguration.MissingPartialTemplateHandler; + public short PartialRecursionDepthLimit => UnderlingConfiguration.PartialRecursionDepthLimit; public Compatibility Compatibility => UnderlingConfiguration.Compatibility; public bool NoEscape => UnderlingConfiguration.NoEscape; diff --git a/source/Handlebars/Configuration/ICompiledHandlebarsConfiguration.cs b/source/Handlebars/Configuration/ICompiledHandlebarsConfiguration.cs index 600e5f21..6434effa 100644 --- a/source/Handlebars/Configuration/ICompiledHandlebarsConfiguration.cs +++ b/source/Handlebars/Configuration/ICompiledHandlebarsConfiguration.cs @@ -36,6 +36,8 @@ public interface ICompiledHandlebarsConfiguration : IHandlebarsTemplateRegistrat IMissingPartialTemplateHandler MissingPartialTemplateHandler { get; } + short PartialRecursionDepthLimit { get; } + IIndexed>> Helpers { get; } IIndexed>> BlockHelpers { get; } diff --git a/source/Handlebars/Pools/BindingContext.Pool.cs b/source/Handlebars/Pools/BindingContext.Pool.cs index a5036505..8fc6a999 100644 --- a/source/Handlebars/Pools/BindingContext.Pool.cs +++ b/source/Handlebars/Pools/BindingContext.Pool.cs @@ -33,6 +33,7 @@ public BindingContext CreateContext(ICompiledHandlebarsConfiguration configurati context.Value = value; context.ParentContext = parent; context.PartialBlockTemplate = partialBlockTemplate; + context.PartialDepth = parent?.PartialDepth ?? 0; context.Initialize();