Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 59 additions & 1 deletion source/Handlebars.Test/InlinePartialTests.cs
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
using Xunit;
using System.Collections.Generic;
using Xunit;

namespace HandlebarsDotNet.Test
{
Expand DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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