mirror of
https://github.com/xoofx/markdig.git
synced 2026-07-08 18:16:21 +00:00
Fix configurable nesting depth limit (#892)
This commit is contained in:
@@ -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()`:
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user