FPDI: Memory Exhaustion and Endless Loop in FPDI leads to Denial of Service
Description
Impact
This is a significant Denial of Service (DoS) vulnerability. Any application that uses FPDI to process user-supplied PDF files is at risk. An attacker can upload a small, malicious PDF file that will cause the server-side script to crash due to memory exhaustion or a script time-out. Repeated attacks can lead to sustained service unavailability.
Patches
Fixed as of version 2.6.7
Workarounds
No.
References
No.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
FPDI prior to 2.6.7 is vulnerable to DoS via a malicious PDF causing memory exhaustion or script timeout.
Vulnerability
FPDI versions before 2.6.7 are vulnerable to denial of service (DoS). An attacker can supply a specially crafted PDF file that, when processed by FPDI, causes memory exhaustion or an endless loop leading to script timeout [1][3].
Exploitation
An attacker does not need any special privileges; the vulnerability is exploitable by uploading a malicious PDF to any application that uses FPDI to process user-supplied PDF files. The attack vector is network-based and requires no authentication [1].
Impact
Successful exploitation results in denial of service: the server-side script crashes due to memory exhaustion or script timeout. Repeated attacks can lead to sustained service unavailability [1][3].
Mitigation
The vulnerability is fixed in FPDI version 2.6.7. Users should upgrade immediately. No workarounds are available [1][3].
AI Insight generated on May 21, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected products
1Patches
31695cfcc7e01Merge commit from fork
8 files changed · +67 −12
composer.lock+15 −9 modified@@ -1615,22 +1615,28 @@ }, { "name": "tecnickcom/tcpdf", - "version": "6.11.2", + "version": "6.11.3", "source": { "type": "git", "url": "https://github.com/tecnickcom/TCPDF.git", - "reference": "e1e2ade18e574e963473f53271591edd8c0033ec" + "reference": "b18f6119161019916c5bb07cb8da5205ae5c1b63" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/tecnickcom/TCPDF/zipball/e1e2ade18e574e963473f53271591edd8c0033ec", - "reference": "e1e2ade18e574e963473f53271591edd8c0033ec", + "url": "https://api.github.com/repos/tecnickcom/TCPDF/zipball/b18f6119161019916c5bb07cb8da5205ae5c1b63", + "reference": "b18f6119161019916c5bb07cb8da5205ae5c1b63", "shasum": "" }, "require": { "ext-curl": "*", "php": ">=7.1.0" }, + "suggest": { + "ext-gd": "Enables additional image handling in some workflows.", + "ext-imagick": "Enables additional image format support when available.", + "ext-zlib": "Recommended for compressed streams and related features.", + "tecnickcom/tc-lib-pdf": "Modern replacement for TCPDF for new projects." + }, "type": "library", "autoload": { "classmap": [ @@ -1661,8 +1667,8 @@ "role": "lead" } ], - "description": "TCPDF is a PHP class for generating PDF documents and barcodes.", - "homepage": "http://www.tcpdf.org/", + "description": "Deprecated legacy PDF engine for PHP. For new projects use tecnickcom/tc-lib-pdf.", + "homepage": "https://tcpdf.org", "keywords": [ "PDFD32000-2008", "TCPDF", @@ -1674,15 +1680,15 @@ ], "support": { "issues": "https://github.com/tecnickcom/TCPDF/issues", - "source": "https://github.com/tecnickcom/TCPDF/tree/6.11.2" + "source": "https://github.com/tecnickcom/TCPDF" }, "funding": [ { "url": "https://www.paypal.com/donate/?hosted_button_id=NZUEC5XS8MFBJ", - "type": "custom" + "type": "paypal" } ], - "time": "2026-03-03T08:58:10+00:00" + "time": "2026-04-21T17:00:18+00:00" }, { "name": "theseer/tokenizer",
src/PdfParser/CrossReference/CrossReferenceException.php+5 −0 modified@@ -76,4 +76,9 @@ class CrossReferenceException extends PdfParserException * @var int */ const ENCRYPTED = 0x010C; + + /** + * @var int + */ + const CYCLIC_STRUCTURE = 0x010D; }
src/PdfParser/CrossReference/CrossReference.php+11 −1 modified@@ -64,6 +64,7 @@ public function __construct(PdfParser $parser, $fileHeaderOffset = 0) $offset = $this->findStartXref(); $reader = null; + $offsets = [$offset]; /** @noinspection TypeUnsafeComparisonInspection */ while ($offset != false) { // By doing an unsafe comparsion we ignore faulty references to byte offset 0 try { @@ -83,7 +84,16 @@ public function __construct(PdfParser $parser, $fileHeaderOffset = 0) $this->readers[] = $reader; if (isset($trailer->value['Prev'])) { - $offset = $trailer->value['Prev']->value; + $nextOffset = $trailer->value['Prev']->value; + if (!\in_array($nextOffset, $offsets, true)) { + $offsets[] = $nextOffset; + $offset = $nextOffset; + } else { + throw new CrossReferenceException( + 'Cross-references includes cyclic structure.', + CrossReferenceException::CYCLIC_STRUCTURE + ); + } } else { $offset = false; }
src/PdfReader/Page.php+13 −2 modified@@ -123,7 +123,13 @@ public function getAttribute($name, $inherited = true) }); if (\count($inheritedKeys) > 0) { - $parentDict = PdfType::resolve(PdfDictionary::get($dict, 'Parent'), $this->parser); + $ensuredObjectList = []; + $parentDict = PdfType::resolve( + PdfDictionary::get($dict, 'Parent'), + $this->parser, + false, + $ensuredObjectList + ); while ($parentDict instanceof PdfDictionary) { foreach ($inheritedKeys as $index => $key) { if (isset($parentDict->value[$key])) { @@ -134,7 +140,12 @@ public function getAttribute($name, $inherited = true) /** @noinspection NotOptimalIfConditionsInspection */ if (isset($parentDict->value['Parent']) && \count($inheritedKeys) > 0) { - $parentDict = PdfType::resolve(PdfDictionary::get($parentDict, 'Parent'), $this->parser); + $parentDict = PdfType::resolve( + PdfDictionary::get($parentDict, 'Parent'), + $this->parser, + false, + $ensuredObjectList + ); } else { break; }
tests/_files/pdfs/specials/page_parent_loop.pdf+0 −0 addedtests/_files/pdfs/specials/xref_prev_loop.pdf+0 −0 addedtests/functional/PdfParser/CrossReference/CrossReferenceTest.php+9 −0 modified@@ -612,4 +612,13 @@ public function testBehaviourWithWrongObjectTypeAttXrefOffset() $this->expectExceptionCode(CrossReferenceException::INVALID_DATA); new CrossReference($parser); } + + public function testBehaviorWithSameOffsets() + { + $stream = StreamReader::createByFile(__DIR__ . '/../../../_files/pdfs/specials/xref_prev_loop.pdf'); + $parser = new PdfParser($stream); + $this->expectException(CrossReferenceException::class); + $this->expectExceptionCode(CrossReferenceException::CYCLIC_STRUCTURE); + new CrossReference($parser); + } } \ No newline at end of file
tests/functional/PdfReader/PageTest.php+14 −0 modified@@ -4,6 +4,7 @@ use PHPUnit\Framework\TestCase; use setasign\Fpdi\PdfParser\PdfParser; +use setasign\Fpdi\PdfParser\PdfParserException; use setasign\Fpdi\PdfParser\StreamReader; use setasign\Fpdi\PdfReader\DataStructure\Rectangle; use setasign\Fpdi\PdfReader\PdfReader; @@ -134,4 +135,17 @@ public function testGetExternalLinks($path, $expectedData) } } } + + public function testGetAttributeWithRecursion() + { + $stream = StreamReader::createByFile(__DIR__ . '/../../_files/pdfs/specials/page_parent_loop.pdf'); + $parser = new PdfParser($stream); + + $pdfReader = new PdfReader($parser); + $page = $pdfReader->getPage(1); + + $this->expectException(PdfParserException::class); + $this->expectExceptionMessage('Indirect reference recursion detected (4).'); + $page->getAttribute('Rotate'); + } }
7d101f321483Fixed handling of recursion in faulty page tree.
1 file changed · +13 −2
src/PdfReader/Page.php+13 −2 modified@@ -123,7 +123,13 @@ public function getAttribute($name, $inherited = true) }); if (\count($inheritedKeys) > 0) { - $parentDict = PdfType::resolve(PdfDictionary::get($dict, 'Parent'), $this->parser); + $ensuredObjectList = []; + $parentDict = PdfType::resolve( + PdfDictionary::get($dict, 'Parent'), + $this->parser, + false, + $ensuredObjectList + ); while ($parentDict instanceof PdfDictionary) { foreach ($inheritedKeys as $index => $key) { if (isset($parentDict->value[$key])) { @@ -134,7 +140,12 @@ public function getAttribute($name, $inherited = true) /** @noinspection NotOptimalIfConditionsInspection */ if (isset($parentDict->value['Parent']) && \count($inheritedKeys) > 0) { - $parentDict = PdfType::resolve(PdfDictionary::get($parentDict, 'Parent'), $this->parser); + $parentDict = PdfType::resolve( + PdfDictionary::get($parentDict, 'Parent'), + $this->parser, + false, + $ensuredObjectList + ); } else { break; }
13521ef3d6a5Fixed handling of /Prev value in view to recursion
2 files changed · +16 −1
src/PdfParser/CrossReference/CrossReferenceException.php+5 −0 modified@@ -76,4 +76,9 @@ class CrossReferenceException extends PdfParserException * @var int */ const ENCRYPTED = 0x010C; + + /** + * @var int + */ + const CYCLIC_STRUCTURE = 0x010D; }
src/PdfParser/CrossReference/CrossReference.php+11 −1 modified@@ -64,6 +64,7 @@ public function __construct(PdfParser $parser, $fileHeaderOffset = 0) $offset = $this->findStartXref(); $reader = null; + $offsets = [$offset]; /** @noinspection TypeUnsafeComparisonInspection */ while ($offset != false) { // By doing an unsafe comparsion we ignore faulty references to byte offset 0 try { @@ -83,7 +84,16 @@ public function __construct(PdfParser $parser, $fileHeaderOffset = 0) $this->readers[] = $reader; if (isset($trailer->value['Prev'])) { - $offset = $trailer->value['Prev']->value; + $nextOffset = $trailer->value['Prev']->value; + if (!\in_array($nextOffset, $offsets, true)) { + $offsets[] = $nextOffset; + $offset = $nextOffset; + } else { + throw new CrossReferenceException( + 'Cross-references includes cyclic structure.', + CrossReferenceException::CYCLIC_STRUCTURE + ); + } } else { $offset = false; }
Vulnerability mechanics
Root cause
"Missing cycle detection in PDF cross-reference table traversal and page-tree parent resolution allows an attacker to craft a PDF that causes infinite loops, leading to memory exhaustion or script timeout."
Attack vector
An attacker uploads a small, specially crafted PDF file to any application that uses FPDI to process user-supplied PDFs. The malicious PDF contains either a cyclic cross-reference table (via repeated /Prev trailer pointers) or a cyclic page-tree parent chain (via repeated /Parent dictionary entries). When FPDI parses the file, the traversal loops indefinitely, consuming CPU and memory until the script crashes or times out. Repeated submissions can cause sustained denial of service. No authentication or special network position is required beyond the ability to upload a PDF.
Affected code
The vulnerability spans two code paths. In `src/PdfParser/CrossReference/CrossReference.php`, the constructor follows /Prev trailer pointers without tracking visited offsets, enabling cyclic cross-reference loops. In `src/PdfReader/Page.php`, the `getAttribute()` method traverses /Parent dictionary entries without cycle detection, enabling cyclic page-tree loops. Both are exercised when parsing a user-supplied PDF.
What the fix does
The patches add cycle detection in two places. In `CrossReference.php` [patch_id=898941], the parser now tracks all previously seen cross-reference offsets in an `$offsets` array; if a /Prev value points to an offset already visited, it throws a new `CrossReferenceException::CYCLIC_STRUCTURE` instead of looping forever. In `Page.php` [patch_id=898942][patch_id=898943], the `getAttribute` method passes an `$ensuredObjectList` accumulator to `PdfType::resolve`; when the same parent dictionary is resolved again, the resolver detects the recursion and throws a `PdfParserException`. Both changes convert unbounded loops into bounded, exception-based failures, preventing memory exhaustion.
Preconditions
- inputAttacker must be able to upload or supply a PDF file to an application using FPDI.
- configThe application must use FPDI to parse the user-supplied PDF (no special configuration required; default usage is vulnerable).
Generated on May 20, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
2News mentions
0No linked articles in our index yet.