Oct 31, 2026·11 min read·2 visits
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.
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.
pypdf is a widely deployed pure-Python library used for reading, splitting, merging, and manipulating PDF documents. When a PDF document is opened via the PdfReader class, the library must locate and parse the document's cross-reference (XRef) tables or streams to map indirect object identifiers to their byte offsets or containing object streams. Because pypdf is frequently integrated into backend web services, document processing pipelines, and automated ingestion systems, its parsing routines represent a primary attack surface when handling untrusted user uploads.
Starting with PDF 1.5, the specification allows cross-reference data to be stored within compressed stream objects (/Type /XRef) rather than traditional plain-text tables. In pypdf versions prior to 6.21.3, the methods responsible for parsing these cross-reference streams—specifically _read_pdf15_xref_stream and _sanitize_pdf15_xref_stream_index_pairs in pypdf/_reader.py—contain resource accounting flaws classified under CWE-400 (Uncontrolled Resource Consumption) and CWE-770 (Allocation of Resources Without Limits or Throttling).
Specifically, the parser fails to enforce a cumulative, document-level ceiling on the total number of cross-reference entries declared across multiple chained /Type /XRef streams. Furthermore, the input sanitization routine permits negative integer values for entry counts inside the /Index dictionary array, which corrupts the accumulator used to enforce per-stream entry limits.
An attacker can exploit these weaknesses by supplying a small, specially crafted PDF document utilizing /Filter /FlateDecode compression, chained /Prev offsets, or negative /Index subsection counts. When pypdf parses the cross-reference structure, it allocates millions of dictionary entries in memory (self.xref and self.xref_objStm), exhausting available heap memory and terminating the host Python process with a MemoryError or triggering the operating system's Out-Of-Memory (OOM) killer.
To understand the root cause of CVE-2026-119209, it is necessary to examine how PDF 1.5 cross-reference streams define object subsections. An /Type /XRef stream dictionary contains several control parameters: /Size (indicating one greater than the highest object number used), /W (an array of integers defining the byte widths of the fields within each cross-reference entry), /Prev (an optional byte offset pointing to an earlier cross-reference stream in incrementally updated documents), and /Index (an optional array of integer pairs [start0, count0, start1, count1, ...]). Each pair in /Index defines the starting object number and the count of consecutive objects in that subsection.
The first flaw stems from the scope of resource clamping during /Prev chain traversal. In _read_xref_tables_and_trailers(), pypdf iterates through a while startxref is not None: loop, following /Prev pointers to reconstruct the document's incremental update history. For each XRef stream encountered, _sanitize_pdf15_xref_stream_index_pairs() clamped the declared /Index counts against max_entries, a value derived from the decompressed byte length of that single stream divided by the entry byte width (/W). While this prevented a single stream from declaring more entries than its decompressed payload could hold, no mechanism tracked the aggregate number of entries across all streams in the /Prev chain. Combined with /Filter /FlateDecode, an attacker can compress megabytes of repetitive null or single-byte entries into a tiny payload and chain multiple XRef streams together, multiplying memory allocations linearly with the chain length.
The second flaw is an integer sign validation oversight within _sanitize_pdf15_xref_stream_index_pairs(). The method iterates over index_pairs and checks odd indices (index % 2 == 1), which represent the object count for each subsection. It maintains a running sum in the variable total and checks if total + pair_value_int > max_entries: to determine whether to clamp the count or raise a LimitReachedError. However, the code failed to verify whether pair_value_int was less than zero.
When an attacker supplies a negative count in the /Index array—for example, /Index [0 -5000000 10 5000000]—the expression total + pair_value_int subtracts from the total accumulator, driving it into a deep negative value. When the subsequent positive count (5000000) is evaluated in the next pair, total + pair_value_int evaluates to 0 (or remains well below max_entries), completely bypassing the per-stream size clamp.
The remediation for CVE-2026-119209 was implemented in commit a93fe3a901fd00af38f0a105c3ba7175fa72dcb4 (Pull Request #4269) within pypdf/_reader.py. The patch addresses both the per-stream negative integer bypass and the lack of a document-wide cross-reference entry budget.
First, PdfReader.__init__ was updated to accept a configurable parameter, xref_stream_maximum_entries, defaulting to 2_000_000. An internal counter, self._xref_stream_entry_count, was added to the instance state and is explicitly reset to 0 at the beginning of _read_xref_tables_and_trailers() before the /Prev traversal loop begins.
# pypdf/_reader.py - Initialization and State Reset
def __init__(
self,
stream: Union[str, StreamType, Path],
strict: bool = False,
password: Union[str, bytes, None] = None,
*,
root_object_recovery_limit: Optional[int] = 10_000,
xref_stream_maximum_entries: int = 2_000_000, # Added document-wide ceiling
) -> None:
# ...
self._xref_stream_maximum_entries = xref_stream_maximum_entries
self._xref_stream_entry_count = 0Second, within _sanitize_pdf15_xref_stream_index_pairs(), the patch inserts an explicit boundary check before updating the total accumulator. When iterating over the count elements of the /Index array (if index % 2 == 1:), the code now checks if pair_value_int < 0:. If the reader is operating in strict mode (self.strict == True), it immediately raises a PdfStreamError. In non-strict mode, it logs a warning and clamps pair_value_int to 0, preventing the accumulator from being decremented.
# pypdf/_reader.py - _sanitize_pdf15_xref_stream_index_pairs()
if index % 2 == 1:
# FIX: Reject or clamp negative count values before checking max_entries
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(
f"Number of entries in XRef stream exceeds limit of {max_entries} in strict mode."
)Third, inside _read_pdf15_xref_stream(), immediately after sanitizing index_pairs, the parser sums all count values (index_pairs[1::2]) and adds them to self._xref_stream_entry_count. If the cumulative count across all XRef streams processed so far exceeds self._xref_stream_maximum_entries, pypdf aborts parsing and raises a LimitReachedError before reading the stream data or populating self.xref.
# pypdf/_reader.py - _read_pdf15_xref_stream()
index_pairs = self._sanitize_pdf15_xref_stream_index_pairs(
index_pairs=index_pairs, entry_sizes=entry_sizes, xref_stream=xref_stream
)
# FIX: Enforce cumulative document-wide limit across all chained /Prev streams
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}."
)Fix Completeness Assessment: The fix effectively neutralizes both attack variations. By clamping negative /Index counts to zero prior to accumulator addition, sum(index_pairs[1::2]) is guaranteed to be a sum of non-negative integers. Furthermore, because _read_xref_tables_and_trailers() tracks visited_xref_offsets to prevent cyclic /Prev loops and _xref_stream_entry_count monotonically increases across all valid /Prev hops prior to entry instantiation, an attacker cannot bypass the 2_000_000 cumulative ceiling via cross-reference streams.
Exploiting CVE-2026-119209 requires no authentication or elevated privileges if the target application exposes an interface that accepts and parses user-supplied PDF files using pypdf.PdfReader. Because PdfReader parses cross-reference tables and streams during object initialization (PdfReader(stream)), the vulnerability is triggered immediately upon opening the file, even if the application never extracts text, metadata, or pages.
To construct an exploit payload, an attacker uses the /Filter /FlateDecode compression mechanism alongside minimal entry widths (/W [1 0 0], meaning each cross-reference entry occupies only 1 byte in the uncompressed stream). Compressing a buffer of 1,000,000 identical bytes (b"\x02" * 1000000) via zlib/FlateDecode produces a compressed stream of roughly 1 kilobyte. When decompressed by pypdf, this single kilobyte satisfies the byte-length check for 1,000,000 XRef entries.
from io import BytesIO
from typing import Optional
from pypdf.filters import FlateDecode
def generate_pdf15_xref_stream_pdf(
size: int, encoded: bytes, index: Optional[str] = None, prev: bool = False
) -> bytes:
pdf = bytearray(b"%PDF-1.7\n")
pdf += b"1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n"
pdf += b"2 0 obj\n<< /Type /Pages /Kids [] /Count 0 >>\nendobj\n"
prev_offset = len(pdf) if prev else None
if prev:
pdf += (
f"3 0 obj\n<< /Type /XRef /Size 12 /Index [10 2] /W [1 0 0] /Root 1 0 R /Filter /FlateDecode "
f"/Length {len(encoded)} >>\nstream\n"
).encode()
pdf += encoded + b"\nendstream\nendobj\n"
startxref = len(pdf)
index_entry = f"/Index {index} " if index is not None else ""
prev_entry = f"/Prev {prev_offset} " if prev_offset is not None else ""
pdf += (
f"3 0 obj\n<< /Type /XRef /Size {size} {index_entry}/W [1 0 0] /Root 1 0 R "
f"{prev_entry}/Filter /FlateDecode /Length {len(encoded)} >>\nstream\n"
).encode()
pdf += encoded + b"\nendstream\nendobj\n"
pdf += f"startxref\n{startxref}\n%%EOF\n".encode()
return bytes(pdf)Using the generator pattern from the pypdf regression test suite (tests/test_reader.py), two distinct exploitation methodologies can be executed against versions prior to 6.21.3:
/Prev Multiplication: The attacker encodes a highly compressible buffer (e.g., FlateDecode.encode(b"\x02" * 1000000)) and chains multiple /Type /XRef objects via /Prev offsets, each assigning non-overlapping object ID ranges in /Index (e.g., [0 1000000], [1000000 1000000], etc.). For each stream in the chain, pypdf allocates 1,000,000 Python integer keys and tuple values in self.xref_objStm or self.xref, quickly consuming gigabytes of RAM from a file only tens of kilobytes in size./Index Clamp Bypass: The attacker sets /Index [0 -5000000 10 5000000] inside a small XRef stream. Because -5000000 is neglected by Python's range(start_id, start_id + count) iteration (as range(0, -5000000) yields an empty iterator), the negative subsection costs zero CPU time while reducing the sanity accumulator total to -5000000. The subsequent [10 5000000] pair then passes validation even if the stream contains almost no data.The primary security impact of CVE-2026-119209 is a high-impact Denial of Service (DoS) affecting system availability, with no direct impact on confidentiality or integrity. When pypdf processes a malicious cross-reference stream, it iterates over range(start_id, start_id + count) for every subsection and populates Python dictionaries (self.xref and self.xref_objStm). Because Python int objects, tuple entries, and dict hash table buckets carry significant per-object memory overhead (typically puluhan bytes per entry), millions of entries translate into hundreds of megabytes or several gigabytes of memory allocation per request.
There is a notable divergence between the CVSS v4.0 score assigned by GitHub (the CNA) and the CVSS v3.1 score assigned by the National Vulnerability Database (NVD):
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:P/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N. This assessment models a network-accessible scenario (or remote delivery where passive user/application interaction processes the file) resulting in high availability impact (VA:H).CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H. NVD models pypdf as a local library requiring local access and low privileges (AV:L/PR:L), though in practice, web applications exposing PDF upload endpoints bridge this vector to AV:N.According to FIRST Exploit Prediction Scoring System (EPSS) metrics, CVE-2026-119209 holds an EPSS score of 0.00319 (0.32%), placing it in the 24.86th percentile. It is not listed in the CISA Known Exploited Vulnerabilities (KEV) catalog. While automated mass exploitation is unlikely for a library-level DoS flaw, synchronous Python web workers (such as Gunicorn, uWSGI, or Celery tasks) that parse untrusted PDFs without strict memory sandboxing are highly susceptible to worker pool exhaustion and container crashes.
To remediate CVE-2026-119209, organizations and developers should upgrade pypdf to version 6.21.3 or later. Version 6.21.3 enforces a default ceiling of 2,000,000 cumulative cross-reference stream entries per document and rejects or clamps negative /Index counts.
pip install --upgrade "pypdf>=6.21.3"For environments with tight memory constraints or microservices that only expect small, standard PDF documents (such as single-page invoices or receipts), the default limit of 2_000_000 entries may still allow more memory allocation than desired during concurrent request spikes. After upgrading to >= 6.21.3, developers should tune the xref_stream_maximum_entries parameter and enable strict=True when instantiating PdfReader:
from pypdf import PdfReader
from pypdf.errors import LimitReachedError, PdfStreamError
try:
# Enforce a stricter limit (e.g., 50,000 entries) and reject malformed streams
reader = PdfReader(
"untrusted_upload.pdf",
strict=True,
xref_stream_maximum_entries=50_000,
root_object_recovery_limit=1_000
)
except (LimitReachedError, PdfStreamError) as exc:
# Handle rejected/malformed PDF gracefully
passIf an immediate upgrade to 6.21.3 is not possible, applications must implement defense-in-depth controls before passing files to pypdf. Specifically, PDF parsing tasks should be isolated in dedicated subprocesses or ephemeral worker containers configured with strict operating system memory limits (such as Linux cgroups, Docker --memory flags, or Python's resource.setrlimit(resource.RLIMIT_AS, ...) on Unix platforms) alongside execution timeouts so that an out-of-memory condition does not terminate the primary application server.
CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H| Product | Affected Versions | Fixed Version |
|---|---|---|
pypdf py-pdf | < 6.21.3 | 6.21.3 |
| Attribute | Detail |
|---|---|
| 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 |
The product does not properly control the allocation and maintenance of a limited resource, thereby enabling an actor to influence the amount of resources consumed, eventually leading to the exhaustion of available resources.
A vulnerability classified under CWE-407 (Inefficient Algorithmic Complexity) exists in the pyasn1 library prior to version 0.6.5. When parsing constructed ASN.1 BIT STRING payloads under BER, CER, or schema-driven DER encoding rules, the BitStringPayloadDecoder repeatedly shifts and copies the entire accumulated bit value for each incoming fragment. Processing inputs with a large number of small constructed fragments induces quadratic CPU consumption, allowing unauthenticated remote attackers to cause a denial of service via resource exhaustion during the initial decoding phase.
CVE-2026-119208 is an uncontrolled resource consumption vulnerability in the pypdf Python library prior to version 6.21.3. When processing an encrypted PDF file after successful decryption, the get_num_pages() method fails to verify whether the encryption handler has already unlocked the document. Consequently, pypdf returns the unvalidated /Count integer directly from the /Pages catalog dictionary instead of traversing and flattening the actual page tree. Attackers can supply an encrypted PDF with an artificially inflated /Count attribute to trigger excessive memory allocation and prolonged CPU usage during downstream operations such as page iteration or label resolution.
CVE-2026-119207 (GHSA-4vqv-7265-gm2w) is an asymmetric resource consumption and denial-of-service (DoS) vulnerability in the open-source Python PDF manipulation library pypdf. In versions prior to 6.21.3, the DictionaryObject.read_from_stream method fails to bound exception message lengths when encountering malformed, unclosed dictionary structures in strict mode. When nested dictionary parsing fails, each level catches the inner exception and raises a new PdfReadError initialized with the string representation (exc.__repr__()) of the caught exception. This recursive wrapping causes geometric string expansion across the call stack, enabling a minimal PDF payload of a few hundred bytes to exhaust gigabytes of heap memory and crash the host Python process.
CVE-2026-119206 is an algorithmic complexity vulnerability in pypdf prior to version 6.21.3 that causes uncontrolled CPU resource consumption when evaluating the page_labels property. Due to repeated linear scans of the PDF catalog's /PageLabels number tree from index zero for each individual page, processing a document with a large number of pages and label ranges scales quadratically. An unauthenticated attacker can supply a crafted PDF document to an application that reads page labels, resulting in prolonged execution times and denial of service.
Authlib versions 0.15 through 1.9.0 contain an authorization bypass vulnerability (CWE-639) in the RFC 7592 ClientConfigurationEndpoint implementation and its corresponding reference documentation. The framework fails to verify that the presented registration access token belongs to the target client specified in the request, and the official reference implementations for authenticate_client and check_permission omit token-to-client binding checks. Consequently, an authenticated client holding any valid registration access token can read, modify, or delete the registration metadata of arbitrary clients on the authorization server.
CVE-2026-118773 (GHSA-3g3m-2fjr-6pw4) is an uncontrolled resource consumption vulnerability in the node-glob npm package affecting versions 9.0.0 through 14.0.1. Due to missing return statements in the AbortSignal check routines within the directory traversal walker, aborted operations reject the caller promise as expected but continue executing background filesystem reads and pattern matching. Applications that rely on AbortSignal or AbortSignal.timeout() to enforce execution budgets on user-influenced glob operations remain susceptible to CPU, event loop, and disk I/O exhaustion.