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
63 changes: 63 additions & 0 deletions src/UglyToad.PdfPig.Core/StackDepthGuard.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
namespace UglyToad.PdfPig.Core
{
/// <summary>
/// Provides a guard for tracking and limiting the depth of nested stack operations, such as recursive calls or
/// nested parsing.
/// </summary>
/// <remarks>Use this class to prevent excessive stack usage by enforcing a maximum nesting depth. This is
/// particularly useful in scenarios where untrusted or deeply nested input could cause stack overflows or
/// performance issues.</remarks>
public sealed class StackDepthGuard
{
/// <summary>
/// Represents a stack depth guard with no effective limit on the allowed depth.
/// </summary>
/// <remarks>Use this instance when stack depth restrictions are not required.</remarks>
public static readonly StackDepthGuard Infinite = new StackDepthGuard(int.MaxValue);

private readonly int maxStackDepth;

private int depth;

/// <summary>
/// Initializes a new instance of the StackDepthGuard class with the specified maximum stack depth.
/// </summary>
/// <param name="maxStackDepth">The maximum allowed stack depth for guarded operations. Must be a positive integer.</param>
public StackDepthGuard(int maxStackDepth)
{
if (maxStackDepth <= 0)
{
throw new ArgumentOutOfRangeException(nameof(maxStackDepth));
}
this.maxStackDepth = maxStackDepth;
}

/// <summary>
/// Increments the current nesting depth and checks against the maximum allowed stack depth.
/// </summary>
/// <exception cref="PdfDocumentFormatException">Thrown if the maximum allowed nesting depth is exceeded.</exception>
public void Enter()
{
if (++depth > maxStackDepth)
{
depth--; // Decrement so Exit remains balanced if someone catches this
throw new PdfDocumentFormatException($"Exceeded maximum nesting depth of {maxStackDepth}.");
}
}

/// <summary>
/// Decreases the current depth level by one, ensuring that the depth does not become negative.
/// </summary>
/// <remarks>If the current depth is already zero, calling this method has no effect. This method
/// is typically used to track or manage nested operations or scopes where depth must remain
/// non-negative.</remarks>
public void Exit()
{
depth--;
if (depth < 0)
{
depth = 0;
}
}
}
}
7 changes: 4 additions & 3 deletions src/UglyToad.PdfPig.Fonts/Type1/Parser/Type1FontParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,16 @@ public static class Type1FontParser
private static readonly char[] Separators = [' '];

private static readonly Type1EncryptedPortionParser EncryptedPortionParser = new Type1EncryptedPortionParser();

/// <summary>
/// Parses an embedded Adobe Type 1 font file.
/// </summary>
/// <param name="inputBytes">The bytes of the font program.</param>
/// <param name="length1">The length in bytes of the clear text portion of the font program.</param>
/// <param name="length2">The length in bytes of the encrypted portion of the font program.</param>
/// <param name="stackDepthGuard"></param>
/// <returns>The parsed type 1 font.</returns>
public static Type1Font Parse(IInputBytes inputBytes, int length1, int length2)
public static Type1Font Parse(IInputBytes inputBytes, int length1, int length2, StackDepthGuard stackDepthGuard)
{
// Sometimes the entire PFB file including the header bytes can be included which prevents parsing in the normal way.
var isEntirePfbFile = inputBytes.Peek() == PfbFileIndicator;
Expand All @@ -44,7 +45,7 @@ public static Type1Font Parse(IInputBytes inputBytes, int length1, int length2)
inputBytes = new MemoryInputBytes(ascii);
}

var scanner = new CoreTokenScanner(inputBytes, false);
var scanner = new CoreTokenScanner(inputBytes, false, stackDepthGuard);

if (!scanner.TryReadToken(out CommentToken comment) || !comment.Data.StartsWith("!"))
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ public void ColorspaceParserError()
{
var parser = new CodespaceRangeParser();
var byteArrayInput = new MemoryInputBytes(OtherEncodings.StringAsLatin1Bytes("1 begincodespacerange\nendcodespacerange"));
var tokenScanner = new CoreTokenScanner(byteArrayInput, false);
var tokenScanner = new CoreTokenScanner(byteArrayInput, false, new StackDepthGuard(256));

Assert.True(tokenScanner.MoveNext());
Assert.True(tokenScanner.CurrentToken is NumericToken);
Expand Down
14 changes: 7 additions & 7 deletions src/UglyToad.PdfPig.Tests/Fonts/Type1/Type1FontParserTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ public void CanReadHexEncryptedPortion()
{
var bytes = GetFileBytes("AdobeUtopia.pfa");

Type1FontParser.Parse(new MemoryInputBytes(bytes), 0, 0);
Type1FontParser.Parse(new MemoryInputBytes(bytes), 0, 0, new StackDepthGuard(256));
}

[Fact]
Expand All @@ -20,39 +20,39 @@ public void CanReadBinaryEncryptedPortionOfFullPfb()
// TODO: support reading in these pfb files
var bytes = GetFileBytes("Raleway-Black.pfb");

Type1FontParser.Parse(new MemoryInputBytes(bytes), 0, 0);
Type1FontParser.Parse(new MemoryInputBytes(bytes), 0, 0, new StackDepthGuard(256));
}

[Fact]
public void CanReadCharStrings()
{
var bytes = GetFileBytes("CMBX10.pfa");

Type1FontParser.Parse(new MemoryInputBytes(bytes), 0, 0);
Type1FontParser.Parse(new MemoryInputBytes(bytes), 0, 0, new StackDepthGuard(256));
}

[Fact]
public void CanReadEncryptedPortion()
{
var bytes = GetFileBytes("CMCSC10");

Type1FontParser.Parse(new MemoryInputBytes(bytes), 0, 0);
Type1FontParser.Parse(new MemoryInputBytes(bytes), 0, 0, new StackDepthGuard(256));
}

[Fact]
public void CanReadAsciiPart()
{
var bytes = GetFileBytes("CMBX12");

Type1FontParser.Parse(new MemoryInputBytes(bytes), 0, 0);
Type1FontParser.Parse(new MemoryInputBytes(bytes), 0, 0, new StackDepthGuard(256));
}

[Fact]
public void OutputCmbx10Svgs()
{
var bytes = GetFileBytes("CMBX10");

var result = Type1FontParser.Parse(new MemoryInputBytes(bytes), 0, 0);
var result = Type1FontParser.Parse(new MemoryInputBytes(bytes), 0, 0, new StackDepthGuard(256));

var builder = new StringBuilder("<!DOCTYPE html><html><head></head><body>");
foreach (var charString in result.CharStrings.CharStrings)
Expand All @@ -71,7 +71,7 @@ public void CanReadFontWithCommentsInOtherSubrs()
{
var bytes = GetFileBytes("CMR10");

Type1FontParser.Parse(new MemoryInputBytes(bytes), 0, 0);
Type1FontParser.Parse(new MemoryInputBytes(bytes), 0, 0, new StackDepthGuard(256));
}

private static byte[] GetFileBytes(string name)
Expand Down
14 changes: 14 additions & 0 deletions src/UglyToad.PdfPig.Tests/Integration/GithubIssuesTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,20 @@

public class GithubIssuesTests
{
[Fact]
public void Issue1217()
{
var path = IntegrationHelpers.GetSpecificTestDocumentPath("stackoverflow_error.pdf");

var options = new ParsingOptions()
{
UseLenientParsing = true,
MaxStackDepth = 100
};
var ex = Assert.Throws<PdfDocumentFormatException>(() => PdfDocument.Open(path, options));
Assert.Equal($"Exceeded maximum nesting depth of {options.MaxStackDepth}.", ex.Message);
}

[Fact]
public void Issue1223()
{
Expand Down
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ 0000000576 00000 n
var results = FirstPassParser.Parse(
new FileHeaderOffset(0),
ib.Bytes,
new CoreTokenScanner(ib.Bytes, true));
new CoreTokenScanner(ib.Bytes, true, new StackDepthGuard(256)));

Assert.Equal(2, results.Parts.Count);
Assert.NotNull(results.Trailer);
Expand Down Expand Up @@ -114,7 +114,7 @@ 0000004385 00000 n

var ib = StringBytesTestConverter.Convert(content, false);

var results = FirstPassParser.Parse(new FileHeaderOffset(0), ib.Bytes, new CoreTokenScanner(ib.Bytes, true));
var results = FirstPassParser.Parse(new FileHeaderOffset(0), ib.Bytes, new CoreTokenScanner(ib.Bytes, true, new StackDepthGuard(256)));

var offsets = results.Parts.Select(x => x.Offset).OrderBy(x => x).ToList();

Expand All @@ -123,7 +123,7 @@ 0000004385 00000 n
Assert.NotNull(results.Trailer);

ib.Bytes.Seek(98);
var scanner = new CoreTokenScanner(ib.Bytes, false);
var scanner = new CoreTokenScanner(ib.Bytes, false, new StackDepthGuard(256));
scanner.MoveNext();
Assert.Equal(scanner.CurrentToken, OperatorToken.Xref);
}
Expand Down
4 changes: 2 additions & 2 deletions src/UglyToad.PdfPig.Tests/Parser/PageContentParserTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

public class PageContentParserTests
{
private readonly PageContentParser parser = new PageContentParser(ReflectionGraphicsStateOperationFactory.Instance);
private readonly PageContentParser parser = new PageContentParser(ReflectionGraphicsStateOperationFactory.Instance, new StackDepthGuard(256));
private readonly ILog log = new NoOpLog();

[Fact]
Expand Down Expand Up @@ -210,7 +210,7 @@ public void CorrectlyHandlesFile0007511CorruptInlineImage()
var content = File.ReadAllText(path);
var input = StringBytesTestConverter.Convert(content, false);

var lenientParser = new PageContentParser(ReflectionGraphicsStateOperationFactory.Instance, true);
var lenientParser = new PageContentParser(ReflectionGraphicsStateOperationFactory.Instance, new StackDepthGuard(256), true);
var result = lenientParser.Parse(1, input.Bytes, log);

Assert.NotEmpty(result);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ public void Issue334()

var bytes = new MemoryInputBytes(input);

var scanner = new CoreTokenScanner(bytes, true, ScannerScope.None);
var scanner = new CoreTokenScanner(bytes, true, new StackDepthGuard(256), ScannerScope.None);

var result = FileHeaderParser.Parse(scanner, bytes, false, log);

Expand Down
17 changes: 9 additions & 8 deletions src/UglyToad.PdfPig.Tests/Parser/Parts/FileTrailerParserTests.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
namespace UglyToad.PdfPig.Tests.Parser.Parts;

using PdfPig.Core;
using PdfPig.Parser.FileStructure;
using PdfPig.Tokenization.Scanner;

Expand All @@ -26,7 +27,7 @@ 12 0 obj

var result = FirstPassParser.GetFirstCrossReferenceOffset(
input.Bytes,
new CoreTokenScanner(input.Bytes, true),
new CoreTokenScanner(input.Bytes, true, new StackDepthGuard(256)),
new TestingLog());

Assert.Equal(456, result.StartXRefDeclaredOffset);
Expand Down Expand Up @@ -59,7 +60,7 @@ 12 0 obj

var result = FirstPassParser.GetFirstCrossReferenceOffset(
input.Bytes,
new CoreTokenScanner(input.Bytes, true),
new CoreTokenScanner(input.Bytes, true, new StackDepthGuard(256)),
new TestingLog());

Assert.Equal(17, result.StartXRefDeclaredOffset);
Expand Down Expand Up @@ -93,7 +94,7 @@ 12 0 obj

var result = FirstPassParser.GetFirstCrossReferenceOffset(
input.Bytes,
new CoreTokenScanner(input.Bytes, true),
new CoreTokenScanner(input.Bytes, true, new StackDepthGuard(256)),
new TestingLog());

Assert.Equal(1384733, result.StartXRefDeclaredOffset);
Expand All @@ -106,7 +107,7 @@ public void BadInputBytesReturnsNull()

var result = FirstPassParser.GetFirstCrossReferenceOffset(
input.Bytes,
new CoreTokenScanner(input.Bytes, true),
new CoreTokenScanner(input.Bytes, true, new StackDepthGuard(256)),
new TestingLog());

Assert.Null(result.StartXRefDeclaredOffset);
Expand All @@ -130,7 +131,7 @@ 11 0 obj

var result = FirstPassParser.GetFirstCrossReferenceOffset(
input.Bytes,
new CoreTokenScanner(input.Bytes, true),
new CoreTokenScanner(input.Bytes, true, new StackDepthGuard(256)),
new TestingLog());

Assert.Null(result.StartXRefDeclaredOffset);
Expand All @@ -151,7 +152,7 @@ 1 0 obj

var result = FirstPassParser.GetFirstCrossReferenceOffset(
input.Bytes,
new CoreTokenScanner(input.Bytes, true),
new CoreTokenScanner(input.Bytes, true, new StackDepthGuard(256)),
new TestingLog());

Assert.Null(result.StartXRefDeclaredOffset);
Expand Down Expand Up @@ -185,7 +186,7 @@ 12 0 obj

var result = FirstPassParser.GetFirstCrossReferenceOffset(
input.Bytes,
new CoreTokenScanner(input.Bytes, true),
new CoreTokenScanner(input.Bytes, true, new StackDepthGuard(256)),
new TestingLog());

Assert.Equal(1274665676543, result.StartXRefDeclaredOffset);
Expand All @@ -207,7 +208,7 @@ public void CanReadStartXrefIfCommentsPresent()

var result = FirstPassParser.GetFirstCrossReferenceOffset(
input.Bytes,
new CoreTokenScanner(input.Bytes, true),
new CoreTokenScanner(input.Bytes, true, new StackDepthGuard(256)),
new TestingLog());

Assert.Equal(57695, result.StartXRefDeclaredOffset);
Expand Down
2 changes: 1 addition & 1 deletion src/UglyToad.PdfPig.Tests/StringBytesTestConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public class Result
internal static (CoreTokenScanner scanner, IInputBytes bytes) Scanner(string s)
{
var inputBytes = new MemoryInputBytes(OtherEncodings.StringAsLatin1Bytes(s));
var result = new CoreTokenScanner(inputBytes, true);
var result = new CoreTokenScanner(inputBytes, true, new StackDepthGuard(256));

return (result, inputBytes);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
namespace UglyToad.PdfPig.Tests.Tokenization
{
using PdfPig.Core;
using PdfPig.Tokenization;
using PdfPig.Tokens;

public class ArrayTokenizerTests
{
private readonly ArrayTokenizer tokenizer = new ArrayTokenizer(true);
private readonly ArrayTokenizer tokenizer = new ArrayTokenizer(true, new StackDepthGuard(256));

[Theory]
[InlineData("]")]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ namespace UglyToad.PdfPig.Tests.Tokenization

public class DictionaryTokenizerTests
{
private readonly DictionaryTokenizer tokenizer = new DictionaryTokenizer(true);
private readonly DictionaryTokenizer tokenizer = new DictionaryTokenizer(true, new StackDepthGuard(256));

[Theory]
[InlineData("[rjee]")]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ public class CoreTokenScannerTests

public CoreTokenScannerTests()
{
scannerFactory = x => new CoreTokenScanner(x, true);
scannerFactory = x => new CoreTokenScanner(x, true, new StackDepthGuard(256));
}

[Fact]
Expand Down Expand Up @@ -231,7 +231,7 @@ public void SkipsCommentsInStreams()

var scanner = new CoreTokenScanner(
StringBytesTestConverter.Convert(content, false).Bytes,
true,
true, new StackDepthGuard(256),
isStream: true);

while (scanner.MoveNext())
Expand All @@ -247,7 +247,7 @@ public void SkipsCommentsInStreams()

var nonStreamScanner = new CoreTokenScanner(
StringBytesTestConverter.Convert(content, false).Bytes,
true,
true, new StackDepthGuard(256),
isStream: false);

while (nonStreamScanner.MoveNext())
Expand Down Expand Up @@ -293,7 +293,7 @@ 0 0 m

var scanner = new CoreTokenScanner(
StringBytesTestConverter.Convert(content, false).Bytes,
true,
true, new StackDepthGuard(256),
isStream: true);

while (scanner.MoveNext())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -726,7 +726,8 @@ private static PdfTokenScanner GetScanner(string s, TestObjectLocationProvider l
new TestFilterProvider(),
NoOpEncryptionHandler.Instance,
new FileHeaderOffset(0),
useLenientParsing ? new ParsingOptions() : ParsingOptions.LenientParsingOff);
useLenientParsing ? new ParsingOptions() : ParsingOptions.LenientParsingOff,
new StackDepthGuard(256));
}

private static IReadOnlyList<ObjectToken> ReadToEnd(PdfTokenScanner scanner)
Expand Down
Loading
Loading