Skip to content

Commit e5c04fe

Browse files
committed
Expose StackDepthGuard via ISeekableTokenScanner so DirectObjectFinder.Get can enforce the nesting depth limit when resolving indirect references and fix #1274
1 parent ba490bd commit e5c04fe

6 files changed

Lines changed: 79 additions & 15 deletions

File tree

src/UglyToad.PdfPig.Tests/Integration/GithubIssuesTests.cs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,50 @@
1111

1212
public class GithubIssuesTests
1313
{
14+
[Fact]
15+
public void Issues1274()
16+
{
17+
// Minimal PDF with self-referencing object: "1 0 obj 1 0 R"
18+
const string pdf = "255044462d312e300a312030206f626a3c3c2f547970652f4361742e300a312030" +
19+
"206f626a2031203020523e22656e646f626a0a322030206f626a3c33203020525d" +
20+
"2f436f756e7420313e3e656e646f626a0a332030206f626a3c3c2f547970652f50" +
21+
"6167652f4d65646961426f785b30143020363132203739325d2f50617265603020" +
22+
"522f5265736f75726365733c3c2f466f6e743cc2c2c2c23030303030206e200a30" +
23+
"302030206e200a36362030c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2" +
24+
"c23c2f46312034203020523e3e3e3e2f436f6e74656e74732035203020523e3e65" +
25+
"6e646f626a0a352030206f626a3c3c2f547970652f466f6e742f53756274797065" +
26+
"2f54797065312f42617365466f6e742f48656c7665746963613e3e656e536f626a" +
27+
"0a352030206f626a3c3c2f4c656e6774682034343e3e73747265616d0a425b5b5b" +
28+
"5b5b5b5b735b5b5b5b7fff30203730302054642028486500006f2920546a204554" +
29+
"0a656e6473747265616d20656e646f626a0a787265660a3020360a303020363535" +
30+
"33352066200a3030203030303030206e200a35382030306e200a30302030206e20" +
31+
"0a36362030206e200a33332030206e200a747261696c65723c3c2f53697a652036" +
32+
"2f526f6f742031203020523e3e0a7374617274787265660a3432370a2525454f46";
33+
34+
var payload = FromHexString(pdf);
35+
36+
var options = new ParsingOptions { MaxStackDepth = 256 };
37+
using var ms = new MemoryStream(payload);
38+
var ex = Assert.Throws<PdfDocumentFormatException>(() => PdfDocument.Open(ms, options));
39+
Assert.Equal($"Exceeded maximum nesting depth of {options.MaxStackDepth}.", ex.Message);
40+
return;
41+
42+
static byte[] FromHexString(string hexString)
43+
{
44+
if (hexString.Length % 2 != 0)
45+
{
46+
throw new ArgumentException("Hex string must have an even number of characters.", nameof(hexString));
47+
}
48+
49+
byte[] result = new byte[hexString.Length / 2];
50+
for (int i = 0; i < result.Length; i++)
51+
{
52+
result[i] = Convert.ToByte(hexString.Substring(i * 2, 2), 16);
53+
}
54+
return result;
55+
}
56+
}
57+
1458
[Fact]
1559
public void Issues1250()
1660
{

src/UglyToad.PdfPig.Tests/Tokens/TestPdfTokenScanner.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@ namespace UglyToad.PdfPig.Tests.Tokens
88

99
internal class TestPdfTokenScanner : IPdfTokenScanner
1010
{
11+
public StackDepthGuard StackDepthGuard => StackDepthGuard.Infinite;
12+
1113
public Dictionary<IndirectReference, ObjectToken> Objects { get; } = new Dictionary<IndirectReference, ObjectToken>();
14+
1215
public bool MoveNext()
1316
{
1417
throw new NotImplementedException();

src/UglyToad.PdfPig.Tokenization/Scanner/CoreTokenScanner.cs

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,8 @@ public class CoreTokenScanner : ISeekableTokenScanner
5252
/// </remarks>
5353
private readonly bool isStream;
5454

55-
private readonly StackDepthGuard stackDepthGuard;
55+
/// <inheritdoc/>
56+
public StackDepthGuard StackDepthGuard { get; }
5657

5758
/// <summary>
5859
/// Create a new <see cref="CoreTokenScanner"/> from the input.
@@ -68,10 +69,10 @@ public CoreTokenScanner(
6869
{
6970
this.inputBytes = inputBytes ?? throw new ArgumentNullException(nameof(inputBytes));
7071
this.usePdfDocEncoding = usePdfDocEncoding;
71-
this.stackDepthGuard = stackDepthGuard;
72+
this.StackDepthGuard = stackDepthGuard;
7273
this.stringTokenizer = new StringTokenizer(usePdfDocEncoding);
73-
this.arrayTokenizer = new ArrayTokenizer(usePdfDocEncoding, this.stackDepthGuard, useLenientParsing);
74-
this.dictionaryTokenizer = new DictionaryTokenizer(usePdfDocEncoding, this.stackDepthGuard, useLenientParsing: useLenientParsing);
74+
this.arrayTokenizer = new ArrayTokenizer(usePdfDocEncoding, this.StackDepthGuard, useLenientParsing);
75+
this.dictionaryTokenizer = new DictionaryTokenizer(usePdfDocEncoding, this.StackDepthGuard, useLenientParsing: useLenientParsing);
7576
this.scope = scope;
7677
this.namedDictionaryRequiredKeys = namedDictionaryRequiredKeys;
7778
this.useLenientParsing = useLenientParsing;
@@ -106,14 +107,14 @@ public void Seek(long position)
106107
/// <inheritdoc />
107108
public bool MoveNext()
108109
{
109-
stackDepthGuard.Enter();
110+
StackDepthGuard.Enter();
110111
try
111112
{
112113
return MoveNextInternal();
113114
}
114115
finally
115116
{
116-
stackDepthGuard.Exit();
117+
StackDepthGuard.Exit();
117118
}
118119
}
119120

@@ -186,7 +187,7 @@ private bool MoveNextInternal()
186187
&& CurrentToken is NameToken name
187188
&& namedDictionaryRequiredKeys.TryGetValue(name, out var requiredKeys))
188189
{
189-
tokenizer = new DictionaryTokenizer(usePdfDocEncoding, stackDepthGuard, requiredKeys, useLenientParsing);
190+
tokenizer = new DictionaryTokenizer(usePdfDocEncoding, StackDepthGuard, requiredKeys, useLenientParsing);
190191
}
191192
}
192193
else

src/UglyToad.PdfPig.Tokenization/Scanner/ISeekableTokenScanner.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,18 @@
11
namespace UglyToad.PdfPig.Tokenization.Scanner
22
{
3+
using UglyToad.PdfPig.Core;
4+
35
/// <inheritdoc />
46
/// <summary>
57
/// A <see cref="T:UglyToad.PdfPig.Tokenization.Scanner.ITokenScanner" /> that supports seeking in the underlying input data.
68
/// </summary>
79
public interface ISeekableTokenScanner : ITokenScanner
810
{
11+
/// <summary>
12+
/// Gets the guard object used to track and limit stack depth during recursive operations.
13+
/// </summary>
14+
StackDepthGuard StackDepthGuard { get; }
15+
916
/// <summary>
1017
/// Move to the specified position.
1118
/// </summary>

src/UglyToad.PdfPig/Parser/Parts/DirectObjectFinder.cs

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -102,14 +102,22 @@ public static bool TryGet<T>(IToken? token, IPdfTokenScanner scanner, [NotNullWh
102102
/// </summary>
103103
public static T Get<T>(IToken token, IPdfTokenScanner scanner) where T : class, IToken
104104
{
105-
if (token is T result)
105+
scanner.StackDepthGuard.Enter();
106+
try
106107
{
107-
return result;
108-
}
108+
if (token is T result)
109+
{
110+
return result;
111+
}
109112

110-
if (token is IndirectReferenceToken reference)
113+
if (token is IndirectReferenceToken reference)
114+
{
115+
return Get<T>(reference.Data, scanner);
116+
}
117+
}
118+
finally
111119
{
112-
return Get<T>(reference.Data, scanner);
120+
scanner.StackDepthGuard.Exit();
113121
}
114122

115123
throw new PdfDocumentFormatException($"Could not find the object {token} with type {typeof(T).Name} instead, it was found with type {token.GetType().Name}.");

src/UglyToad.PdfPig/Tokenization/Scanner/PdfTokenScanner.cs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,8 @@ internal class PdfTokenScanner : IPdfTokenScanner
5151

5252
public long Length => coreTokenScanner.Length;
5353

54-
private readonly StackDepthGuard stackDepthGuard;
54+
/// <inheritdoc/>
55+
public StackDepthGuard StackDepthGuard { get; }
5556

5657
public PdfTokenScanner(
5758
IInputBytes inputBytes,
@@ -68,7 +69,7 @@ public PdfTokenScanner(
6869
this.encryptionHandler = encryptionHandler;
6970
this.fileHeaderOffset = fileHeaderOffset;
7071
this.parsingOptions = parsingOptions;
71-
this.stackDepthGuard = stackDepthGuard;
72+
this.StackDepthGuard = stackDepthGuard;
7273
coreTokenScanner = new CoreTokenScanner(inputBytes, true, stackDepthGuard, useLenientParsing: parsingOptions.UseLenientParsing);
7374
}
7475

@@ -888,7 +889,7 @@ private IReadOnlyList<ObjectToken> ParseObjectStream(StreamToken stream, XrefLoc
888889
var scanner = new CoreTokenScanner(
889890
bytes,
890891
true,
891-
stackDepthGuard,
892+
StackDepthGuard,
892893
useLenientParsing: parsingOptions.UseLenientParsing,
893894
isStream: true);
894895

0 commit comments

Comments
 (0)