Skip to content

Commit

Permalink
Merge pull request #552 from RoosterDragon/recursion-limit
Browse files Browse the repository at this point in the history
Introduce PartialRecursionDepthLimit
  • Loading branch information
oformaniuk authored Apr 1, 2024
2 parents 9fc63f8 + c727adf commit f3fd1ef
Show file tree
Hide file tree
Showing 8 changed files with 192 additions and 2 deletions.
60 changes: 59 additions & 1 deletion source/Handlebars.Test/InlinePartialTests.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Xunit;
using System.Collections.Generic;
using Xunit;

namespace HandlebarsDotNet.Test
{
Expand Down Expand Up @@ -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<HandlebarsRuntimeException>(Result);
while (ex.InnerException != null)
ex = Assert.IsType<HandlebarsRuntimeException>(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<string, object>();
var items = data;
for (var depth = 0; depth < Handlebars.Configuration.PartialRecursionDepthLimit; depth++)
{
var nestedItems = new Dictionary<string, object>();
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<string, object>();
var items = data;
for (var depth = 0; depth < Handlebars.Configuration.PartialRecursionDepthLimit + 1; depth++)
{
var nestedItems = new Dictionary<string, object>();
items.Add("items", new[] { nestedItems });
items = nestedItems;
}

string Result() => template(data);
var ex = Assert.Throws<HandlebarsRuntimeException>(Result);
while (ex.InnerException != null)
ex = Assert.IsType<HandlebarsRuntimeException>(ex.InnerException);
Assert.Equal("Runtime error while rendering partial 'list', exceeded recursion depth limit of 100", ex.Message);
}
}
}

102 changes: 102 additions & 0 deletions source/Handlebars.Test/PartialTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<HandlebarsRuntimeException>(Result);
Assert.Equal("Runtime error while rendering partial 'myPartial', see inner exception for more information", ex.Message);
ex = Assert.IsType<HandlebarsRuntimeException>(ex.InnerException);
Assert.Equal("Runtime error while rendering partial 'myPartial', see inner exception for more information", ex.Message);
ex = Assert.IsType<HandlebarsRuntimeException>(ex.InnerException);
Assert.Equal("Referenced partial name @partial-block could not be resolved", ex.Message);
Assert.Null(ex.InnerException);
}

[Fact]
public void TemplateWithSpecialNamedPartial()
{
Expand Down Expand Up @@ -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<HandlebarsRuntimeException>(Result);
while (ex.InnerException != null)
ex = Assert.IsType<HandlebarsRuntimeException>(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<string, object>();
var items = data;
for (var depth = 0; depth < Handlebars.Configuration.PartialRecursionDepthLimit; depth++)
{
var nestedItems = new Dictionary<string, object>();
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<string, object>();
var items = data;
for (var depth = 0; depth < Handlebars.Configuration.PartialRecursionDepthLimit + 1; depth++)
{
var nestedItems = new Dictionary<string, object>();
items.Add("items", new[] { nestedItems });
items = nestedItems;
}

string Result() => template(data);
var ex = Assert.Throws<HandlebarsRuntimeException>(Result);
while (ex.InnerException != null)
ex = Assert.IsType<HandlebarsRuntimeException>(ex.InnerException);
Assert.Equal("Runtime error while rendering partial 'list', exceeded recursion depth limit of 100", ex.Message);
}
}
}

2 changes: 2 additions & 0 deletions source/Handlebars/BindingContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand All @@ -152,6 +166,7 @@ private static bool InvokePartial(
}
}

IncreaseDepth();
try
{
using var textWriter = writer.CreateWrapper();
Expand All @@ -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--;
}
}
}
}
5 changes: 5 additions & 0 deletions source/Handlebars/Configuration/HandlebarsConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,11 @@ public string UnresolvedBindingFormatter
/// </summary>
public IMissingPartialTemplateHandler MissingPartialTemplateHandler { get; set; }

/// <summary>
/// Maximum depth to recurse into partial templates when evaluating the template. Defaults to 100.
/// </summary>
public short PartialRecursionDepthLimit { get; set; } = 100;

/// <inheritdoc cref="IMemberAliasProvider"/>
public IAppendOnlyList<IMemberAliasProvider> AliasProviders { get; } = new ObservableList<IMemberAliasProvider>();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ public interface ICompiledHandlebarsConfiguration : IHandlebarsTemplateRegistrat

IMissingPartialTemplateHandler MissingPartialTemplateHandler { get; }

short PartialRecursionDepthLimit { get; }

IIndexed<PathInfoLight, Ref<IHelperDescriptor<HelperOptions>>> Helpers { get; }

IIndexed<PathInfoLight, Ref<IHelperDescriptor<BlockHelperOptions>>> BlockHelpers { get; }
Expand Down
1 change: 1 addition & 0 deletions source/Handlebars/Pools/BindingContext.Pool.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down

0 comments on commit f3fd1ef

Please sign in to comment.