Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce PartialRecursionDepthLimit #552

Merged
merged 3 commits into from
Apr 1, 2024
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 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 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 All @@ -84,7 +85,7 @@
where TOptions : struct, IOptions
where TDescriptor : class, IDescriptor<TOptions>
{
var equalityComparer = Compatibility.RelaxedHelperNaming ? PathInfoLight.PlainPathComparer : PathInfoLight.PlainPathWithPartsCountComparer;

Check warning on line 88 in source/Handlebars/Configuration/HandlebarsConfigurationAdapter.cs

View workflow job for this annotation

GitHub Actions / Build

'Compatibility.RelaxedHelperNaming' is obsolete: 'Toggle will be removed in the next major release'

Check warning on line 88 in source/Handlebars/Configuration/HandlebarsConfigurationAdapter.cs

View workflow job for this annotation

GitHub Actions / Build

'Compatibility.RelaxedHelperNaming' is obsolete: 'Toggle will be removed in the next major release'

Check warning on line 88 in source/Handlebars/Configuration/HandlebarsConfigurationAdapter.cs

View workflow job for this annotation

GitHub Actions / SonarCloud

'Compatibility.RelaxedHelperNaming' is obsolete: 'Toggle will be removed in the next major release'

Check warning on line 88 in source/Handlebars/Configuration/HandlebarsConfigurationAdapter.cs

View workflow job for this annotation

GitHub Actions / SonarCloud

'Compatibility.RelaxedHelperNaming' is obsolete: 'Toggle will be removed in the next major release'

Check warning on line 88 in source/Handlebars/Configuration/HandlebarsConfigurationAdapter.cs

View workflow job for this annotation

GitHub Actions / SonarCloud

'Compatibility.RelaxedHelperNaming' is obsolete: 'Toggle will be removed in the next major release'

Check warning on line 88 in source/Handlebars/Configuration/HandlebarsConfigurationAdapter.cs

View workflow job for this annotation

GitHub Actions / Tests on ubuntu-latest

'Compatibility.RelaxedHelperNaming' is obsolete: 'Toggle will be removed in the next major release'

Check warning on line 88 in source/Handlebars/Configuration/HandlebarsConfigurationAdapter.cs

View workflow job for this annotation

GitHub Actions / Tests on macos-latest

'Compatibility.RelaxedHelperNaming' is obsolete: 'Toggle will be removed in the next major release'

Check warning on line 88 in source/Handlebars/Configuration/HandlebarsConfigurationAdapter.cs

View workflow job for this annotation

GitHub Actions / Tests on macos-latest

'Compatibility.RelaxedHelperNaming' is obsolete: 'Toggle will be removed in the next major release'

Check warning on line 88 in source/Handlebars/Configuration/HandlebarsConfigurationAdapter.cs

View workflow job for this annotation

GitHub Actions / Tests on windows-2019

'Compatibility.RelaxedHelperNaming' is obsolete: 'Toggle will be removed in the next major release'

Check warning on line 88 in source/Handlebars/Configuration/HandlebarsConfigurationAdapter.cs

View workflow job for this annotation

GitHub Actions / Run Benchmark.Net

'Compatibility.RelaxedHelperNaming' is obsolete: 'Toggle will be removed in the next major release'
var existingHelpers = source.ToIndexed(
o => (PathInfoLight) $"[{o.Key}]",
o => new Ref<TDescriptor>(o.Value),
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
Loading