CVE-2026-119209: Uncontrolled Resource Consumption in pypdf XRef Stream Parser
Vulnerability ID: CVE-2026-119209
CVSS Score: 5.5
Published: 2026-10-30
CVE-2026-119209 is an uncontrolled resource consumption vulnerability in the pypdf Python library prior to version 6.21.3. The flaw exists within the PDF 1.5 cross-reference (XRef) stream parsing routines in pypdf/_reader.py. Due to the absence of a document-wide entry limit across chained cross-reference streams and improper validation of negative entry counts in the /Index array, an attacker can craft a compact PDF file that triggers excessive memory allocation, resulting in a Denial of Service (DoS) via MemoryError or process termination.
TL;DR
Versions of pypdf prior to 6.21.3 fail to restrict the cumulative number of cross-reference stream entries across chained /Prev streams and allow negative integer counts in /Index arrays to bypass per-stream validation clamps. Processing a crafted PDF causes uncontrolled memory allocation and application crash.
⚠️ Exploit Status: POC
Technical Details
- Vulnerability Type: Uncontrolled Resource Consumption (CWE-400 / CWE-770)
- Affected Component: pypdf._reader.PdfReader (_read_pdf15_xref_stream)
- CVSS Score: 5.5 Medium (NVD v3.1) / 7.1 High (CNA v4.0)
- Attack Vector: Crafted PDF with chained /Prev XRef streams or negative /Index counts
- Security Impact: Denial of Service (DoS) via Memory Exhaustion (OOM)
- EPSS Score: 0.32% (24.86th percentile)
- Exploit Status: Proof-of-Concept (Unit test generator in PR #4269)
- Patched Version: pypdf 6.21.3
Affected Systems
- pypdf (< 6.21.3)
-
pypdf: < 6.21.3 (Fixed in:
6.21.3)
Code Analysis
Commit: a93fe3a
Enforce global xref_stream_maximum_entries limit across XRef streams and reject/clamp negative /Index counts in _reader.py
--- a/pypdf/_reader.py
+++ b/pypdf/_reader.py
@@ -116,6 +116,8 @@ class PdfReader(PdfDocCommon):
root_object_recovery_limit: The maximum number of objects to query
for recovering the Root object in non-strict mode. To disable
this security measure, pass ``None``.
+ xref_stream_maximum_entries: The maximum number of entries which are
+ allowed inside a stream-based cross-reference stream.
"""
@@ -129,6 +131,7 @@ def __init__(
password: Union[str, bytes, None] = None,
*,
root_object_recovery_limit: Optional[int] = 10_000,
+ xref_stream_maximum_entries: int = 2_000_000,
) -> None:
self.strict = strict
self.flattened_pages: Optional[list[PageObject]] = None
@@ -147,6 +150,8 @@ def __init__(
self._root_object_recovery_limit = (
root_object_recovery_limit if isinstance(root_object_recovery_limit, int) else sys.maxsize
)
+ self._xref_stream_maximum_entries = xref_stream_maximum_entries
+ self._xref_stream_entry_count = 0
apply_legacy_configuration()
# Map page indirect_reference number to page number
@@ -1194,6 +1199,7 @@ def _read_xref_tables_and_trailers(
self.xref = {}
self.xref_free_entry = {}
self.xref_objStm = {}
+ self._xref_stream_entry_count = 0
self.trailer = DictionaryObject()
visited_xref_offsets: set[int] = set()
while startxref is not None:
@@ -1351,6 +1357,16 @@ def _sanitize_pdf15_xref_stream_index_pairs(
# `index_pairs` has the format `[start0, count0, start1, count1, ...]`
# Only modify the counts here, but keep the start values.
if index % 2 == 1:
+ if pair_value_int < 0:
+ if self.strict:
+ raise PdfStreamError(f"Negative XRef count {pair_value_int}.")
+ logger_warning(
+ "Negative XRef count %(count)d, using 0.",
+ source=__name__,
+ count=pair_value_int,
+ )
+ pair_value_int = 0
+
if total + pair_value_int > max_entries:
if self.strict:
raise LimitReachedError(
@@ -1395,6 +1411,13 @@ def _read_pdf15_xref_stream(self, stream: StreamType) -> StreamObject:
index_pairs=index_pairs, entry_sizes=entry_sizes, xref_stream=xref_stream
)
+ self._xref_stream_entry_count += sum(index_pairs[1::2])
+ if self._xref_stream_entry_count > self._xref_stream_maximum_entries:
+ raise LimitReachedError(
+ f"Maximum XRef stream entry limit reached: "
+ f"{self._xref_stream_entry_count} > {self._xref_stream_maximum_entries}."
+ )
+
stream_data = BytesIO(xref_stream.get_data())
def get_entry(i: int) -> Union[int, tuple[int, ...]]:
Exploit Details
- GitHub Pull Request #4269 (Official Unit Tests): Contains the regression test suite (tests/test_reader.py) with generate_pdf15_xref_stream_pdf() demonstrating chained /Prev streams and negative /Index counts.
Mitigation Strategies
- Upgrade pypdf to version 6.21.3 or later to enforce document-wide XRef stream entry limits and negative count validation.
- Configure custom lower thresholds using the xref_stream_maximum_entries parameter in PdfReader for memory-constrained environments.
- Enable strict parsing mode (strict=True) in PdfReader to immediately raise exceptions on malformed PDF structures.
- Isolate PDF parsing workloads within memory-constrained worker processes or containers using cgroups or resource.setrlimit.
Remediation Steps:
- Audit project dependencies (requirements.txt, pyproject.toml, Pipfile.lock, or poetry.lock) for pypdf versions prior to 6.21.3.
- Run
pip install --upgrade "pypdf>=6.21.3"across all affected build pipelines and runtime environments. - Review all call sites of
pypdf.PdfReader()that process untrusted inputs and pass an appropriatexref_stream_maximum_entriesvalue (e.g.,xref_stream_maximum_entries=100_000). - Ensure exception handlers around
PdfReadercatchpypdf.errors.LimitReachedErrorandpypdf.errors.PdfStreamErrorto prevent unhandled 500 Internal Server Errors.
References
- GitHub Security Advisory GHSA-x3rh-gjgm-fmqc
- pypdf Pull Request #4269
- Fix Commit a93fe3a901fd00af38f0a105c3ba7175fa72dcb4
- pypdf Release 6.21.3
- National Vulnerability Database - CVE-2026-119209
- CVE.org Record - CVE-2026-119209
- CVE List V5 JSON Record - CVE-2026-119209
Read the full report for CVE-2026-119209 on our website for more details including interactive diagrams and full exploit analysis.
Top comments (0)