Fix configurable nesting depth limit (#892)

This commit is contained in:
Alexandre Mutel
2026-06-13 14:11:47 +02:00
parent b83641351f
commit d7f13b03cf
7 changed files with 108 additions and 16 deletions

View File

@@ -40,8 +40,24 @@ The `MarkdownPipeline` contains:
| Inline parsers | `InlineParserList` | Ordered list of `InlineParser` objects |
| Extensions | `OrderedList<IMarkdownExtension>` | Registered extensions |
| TrackTrivia | `bool` | Whether trivia parsing is enabled |
| MaximumNestingDepth | `int` | Maximum AST nesting depth allowed while parsing and rendering |
| DocumentProcessed | `ProcessDocumentDelegate?` | Callback after parsing completes |
## Nesting depth limit
Markdig protects parsing and rendering with a conservative default nesting limit of 128 levels. If you process trusted input that can legitimately produce deeper trees, configure `MaximumNestingDepth` before calling `Build()`:
```csharp
var pipeline = new MarkdownPipelineBuilder
{
MaximumNestingDepth = 512,
}
.UseListExtras()
.Build();
```
Only raise this for trusted content: a larger limit allows deeper documents but can increase parsing cost and rendering stack usage.
## How Build() works
When you call `builder.Build()`:

View File

@@ -85,6 +85,30 @@ public class MiscTests
}
}
[Test]
public void MaximumNestingDepthCanBeRaisedForDeepListExtras()
{
var markdown = "Krankenhaus\nD. " + string.Join(" ", Enumerable.Repeat("M.", 160));
var pipeline = new MarkdownPipelineBuilder
{
MaximumNestingDepth = 512,
}.UseListExtras().Build();
Assert.DoesNotThrow(() => Markdown.ToHtml(markdown, pipeline));
}
[Test]
public void MaximumNestingDepthCanBeLowered()
{
var pipeline = new MarkdownPipelineBuilder
{
MaximumNestingDepth = 16,
}.Build();
Exception e = Assert.Throws<ArgumentException>(() => Markdown.ToHtml(new string('>', 20), pipeline));
Assert.True(e.Message.Contains("depth limit"));
}
[Test]
public void IsIssue356Corrected()
{

View File

@@ -14,6 +14,16 @@ namespace Markdig.Helpers;
[ExcludeFromCodeCoverage]
internal static class ThrowHelper
{
// Very conservative limit used to limit nesting in the final AST.
// Used to avoid a StackOverflow in the recursive rendering process.
internal const int DefaultDepthLimit = 128;
// Limit used for reducing the maximum execution time for pathological-case inputs.
// Applies to:
// a) inputs that would fail depth checks in the future (for example "[[[[[..." or ">>>>>>...")
// b) very large pipe tables.
internal const int LargeDepthLimit = 10 * 1024;
[DoesNotReturn]
public static void ArgumentNullException(string paramName) => throw new ArgumentNullException(paramName);
@@ -65,18 +75,14 @@ internal static class ThrowHelper
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void CheckDepthLimit(int depth, bool useLargeLimit = false)
{
// Very conservative limit used to limit nesting in the final AST
// Used to avoid a StackOverflow in the recursive rendering process
const int DepthLimit = 128;
int limit = useLargeLimit ? LargeDepthLimit : DefaultDepthLimit;
// Limit used for reducing the maximum execution time for pathological-case inputs
// Applies to:
// a) inputs that would fail depth checks in the future (for example "[[[[[..." or ">>>>>>...")
// b) very large pipe tables
const int LargeDepthLimit = 10 * 1024;
int limit = useLargeLimit ? LargeDepthLimit : DepthLimit;
CheckDepthLimit(depth, limit);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void CheckDepthLimit(int depth, int limit)
{
if (depth > limit)
DepthLimitExceeded();

View File

@@ -63,6 +63,12 @@ public sealed class MarkdownPipeline
/// </summary>
public bool TrackTrivia { get; internal set; }
/// <summary>
/// Gets the maximum nesting depth allowed for the Markdown syntax tree during parsing and rendering.
/// </summary>
/// <remarks>The default value is 128. Raising this value allows deeper documents but can increase parsing cost and rendering stack usage.</remarks>
public int MaximumNestingDepth { get; internal set; } = ThrowHelper.DefaultDepthLimit;
/// <summary>
/// Allows to setup a <see cref="IMarkdownRenderer"/>.
/// </summary>
@@ -70,6 +76,11 @@ public sealed class MarkdownPipeline
public void Setup(IMarkdownRenderer renderer)
{
if (renderer is null) ThrowHelper.ArgumentNullException(nameof(renderer));
if (renderer is RendererBase rendererBase)
{
rendererBase.MaximumNestingDepth = MaximumNestingDepth;
}
foreach (var extension in Extensions)
{
extension.Setup(this, renderer);
@@ -140,4 +151,4 @@ public sealed class MarkdownPipeline
public void Dispose() => _cache.Release(Instance);
}
}
}

View File

@@ -17,6 +17,7 @@ namespace Markdig;
public class MarkdownPipelineBuilder
{
private MarkdownPipeline? _pipeline;
private int _maximumNestingDepth = ThrowHelper.DefaultDepthLimit;
/// <summary>
/// Initializes a new instance of the <see cref="MarkdownPipeline" /> class.
@@ -82,6 +83,22 @@ public class MarkdownPipelineBuilder
/// </summary>
public bool TrackTrivia { get; set; }
/// <summary>
/// Gets or sets the maximum nesting depth allowed for the Markdown syntax tree during parsing and rendering.
/// </summary>
/// <remarks>The default value is 128. Raising this value allows deeper documents but can increase parsing cost and rendering stack usage.</remarks>
/// <exception cref="ArgumentOutOfRangeException">Thrown when the value is less than or equal to zero.</exception>
public int MaximumNestingDepth
{
get => _maximumNestingDepth;
set
{
if (value <= 0) ThrowHelper.ArgumentOutOfRangeException("The maximum nesting depth must be greater than zero.", nameof(value));
_maximumNestingDepth = value;
}
}
/// <summary>
/// Occurs when a document has been processed after the <see cref="MarkdownParser.Parse"/> method.
/// </summary>
@@ -124,6 +141,7 @@ public class MarkdownPipelineBuilder
{
PreciseSourceLocation = PreciseSourceLocation,
TrackTrivia = TrackTrivia,
MaximumNestingDepth = MaximumNestingDepth,
};
return _pipeline;
}

View File

@@ -73,7 +73,7 @@ public static class MarkdownParser
inlineProcessor.DebugLog = pipeline.DebugLog;
try
{
ProcessInlines(inlineProcessor, document);
ProcessInlines(inlineProcessor, document, pipeline.MaximumNestingDepth);
}
finally
{
@@ -149,7 +149,7 @@ public static class MarkdownParser
}
[MethodImpl(MethodImplOptions.NoInlining)]
private static void ProcessInlines(InlineProcessor inlineProcessor, MarkdownDocument document)
private static void ProcessInlines(InlineProcessor inlineProcessor, MarkdownDocument document, int maximumNestingDepth)
{
// "stackless" processor
int blockCount = 1;
@@ -235,8 +235,8 @@ public static class MarkdownParser
if (blockCount == blocks.Length)
{
ThrowHelper.CheckDepthLimit(blockCount + 1, maximumNestingDepth);
Array.Resize(ref blocks, blockCount * 2);
ThrowHelper.CheckDepthLimit(blocks.Length);
}
blocks[blockCount++] = new ContainerItem(Unsafe.As<ContainerBlock>(block));
block.OnProcessInlinesBegin(inlineProcessor);

View File

@@ -33,6 +33,7 @@ public abstract class RendererBase : IMarkdownRenderer
private readonly RendererEntry[][] _renderersPerType;
internal int _childrenDepth = 0;
private int _maximumNestingDepth = ThrowHelper.DefaultDepthLimit;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static IntPtr GetKeyForType(MarkdownObject obj) => Type.GetTypeHandle(obj).Value;
@@ -96,6 +97,22 @@ public abstract class RendererBase : IMarkdownRenderer
/// </summary>
public bool IsLastInContainer { get; private set; }
/// <summary>
/// Gets or sets the maximum nesting depth allowed while rendering Markdown objects.
/// </summary>
/// <remarks>The default value is 128. Raising this value allows deeper documents but can increase rendering stack usage.</remarks>
/// <exception cref="ArgumentOutOfRangeException">Thrown when the value is less than or equal to zero.</exception>
public int MaximumNestingDepth
{
get => _maximumNestingDepth;
set
{
if (value <= 0) ThrowHelper.ArgumentOutOfRangeException("The maximum nesting depth must be greater than zero.", nameof(value));
_maximumNestingDepth = value;
}
}
/// <summary>
/// Occurs before writing an object.
/// </summary>
@@ -117,7 +134,7 @@ public abstract class RendererBase : IMarkdownRenderer
return;
}
ThrowHelper.CheckDepthLimit(_childrenDepth++);
ThrowHelper.CheckDepthLimit(_childrenDepth++, MaximumNestingDepth);
bool saveIsFirstInContainer = IsFirstInContainer;
bool saveIsLastInContainer = IsLastInContainer;
@@ -146,7 +163,7 @@ public abstract class RendererBase : IMarkdownRenderer
return;
}
ThrowHelper.CheckDepthLimit(_childrenDepth++);
ThrowHelper.CheckDepthLimit(_childrenDepth++, MaximumNestingDepth);
bool saveIsFirstInContainer = IsFirstInContainer;
bool saveIsLastInContainer = IsLastInContainer;