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();