CVEReports
CVEReports

Automated vulnerability intelligence platform. Comprehensive reports for high-severity CVEs generated by AI.

Product

  • Home
  • Sitemap
  • RSS Feed

Company

  • About
  • Contact
  • Privacy Policy
  • Terms of Service

© 2026 CVEReports. All rights reserved.

Made with love by Amit Schendel & Alon Barad



CVE-2026-119203

CVE-2026-119203: Infinite Loop in pypdf Standard Cross-Reference Table Parser

Amit Schendel
Amit Schendel
Senior Security Researcher

Oct 31, 2026·9 min read·3 visits

Executive Summary (TL;DR)

Versions of the Python pypdf library prior to 6.21.4 contain an infinite loop vulnerability (CWE-835) in the _read_standard_xref_table method. Processing a malformed PDF with an incomplete cross-reference table that lacks a trailer causes the parser to repeatedly seek backward to the same byte position, hanging the executing thread and causing a denial of service.

CVE-2026-119203 is a denial-of-service vulnerability affecting the pypdf library in versions prior to 6.21.4. The flaw occurs within the standard cross-reference (xref) table parsing routine when processing truncated or malformed PDF streams that terminate before a valid trailer dictionary is encountered. Because the parser attempts to rewind the stream pointer by a fixed offset whenever the trailer token is absent without verifying forward progress, hitting the end-of-file (EOF) or non-advancing tokens causes the parser to re-read the same byte offset indefinitely. An unauthenticated attacker can exploit this flaw by supplying a crafted PDF document to an application using pypdf, resulting in continuous CPU consumption and thread exhaustion.

Vulnerability Overview

pypdf is a widely deployed, pure-Python library used for reading, manipulating, and writing PDF documents across web applications, automated document processing pipelines, and backend services. Because PDF parsing requires interpreting complex binary and text structures—including object streams, indirect references, and cross-reference tables—the parser represents a primary attack surface whenever applications accept untrusted documents from external users.

In versions of pypdf prior to 6.21.4, a logic flaw exists in the PdfReader._read_standard_xref_table method inside pypdf/_reader.py. This method is responsible for parsing traditional PDF cross-reference (xref) tables, which map indirect object numbers to their exact byte offsets within the file stream. According to the PDF specification, a standard cross-reference table consists of one or more subsections followed immediately by the trailer keyword and its associated dictionary.

The vulnerability, tracked as CVE-2026-119203 (and GitHub Security Advisory GHSA-5849-834g-hvw5), is classified under CWE-835 (Loop with Unreachable Exit Condition). When pypdf processes an incomplete or specifically crafted PDF whose cross-reference table reaches the end of the file (EOF) or contains trailing garbage without a valid trailer keyword, the parser fails to advance the stream position. It enters an infinite loop that repeatedly seeks backward and re-evaluates the same byte offset, pinning the executing Python thread at 100% CPU utilization.

Root Cause Analysis

To locate objects within a PDF document, pypdf first reads the startxref offset near the end of the file and seeks to the beginning of the cross-reference section. If the xref keyword is present, PdfReader invokes _read_standard_xref_table(self, stream: StreamType) to iterate through the cross-reference subsections. Each subsection begins with a header specifying the starting object number and the count of contiguous entries, followed by 20-byte lines defining each object's byte offset, generation number, and status (n for in-use, f for free).

Because a standard cross-reference table can be split into multiple contiguous subsections before terminating at the trailer keyword, _read_standard_xref_table wraps the subsection parsing logic inside an unbounded while True: loop. At the end of each subsection iteration, the parser checks whether the stream has reached the trailer keyword by seeking back one byte (stream.seek(-1, 1)) and reading a 7-byte chunk (trailer_tag = stream.read(7)).

The root cause of CVE-2026-119203 is an unchecked assumption regarding stream progression when trailer_tag != b"trailer". The parser assumed that if the next 7 bytes did not match b"trailer", another cross-reference subsection must follow, and it executed stream.seek(-7, 1) to rewind the stream so the next loop iteration could read the subsequent subsection header.

However, if the input stream is truncated or malformed—such as declaring a subsection with zero entries (1 0) followed by fewer than 7 bytes before EOF (e.g., b"aaa")—stream.read(7) reads only the remaining bytes (3 bytes in the case of b"aaa") before hitting EOF. The subsequent relative rewind stream.seek(-7, 1) moves the file pointer 7 bytes backward from EOF, landing inside or prior to the already parsed tokens. In the next iteration of the while True loop, the parser processes the exact same tokens and reaches the exact same stream offset after stream.seek(-7, 1), creating an invariant state where the loop exit condition (trailer_tag == b"trailer") can never be reached.

Code Analysis

The vulnerability was remediated in commit 06593f993789a4b573dd955730c1a7051128bb2a (Pull Request #4289). The modification targets pypdf/_reader.py inside the _read_standard_xref_table method.

Prior to version 6.21.4, the loop termination check at the bottom of _read_standard_xref_table performed a blind relative seek whenever the 7-byte read did not match b"trailer":

# Vulnerable implementation in pypdf < 6.21.4 (pypdf/_reader.py)
def _read_standard_xref_table(self, stream: StreamType) -> None:
    stream.seek(-1, 1)
    first_time = True  # check if the first time looking at the xref table
    recovery_cache: Optional[dict[int, tuple[int, int]]] = None
    while True:
        num = cast(int, read_object(stream, self))
        # ... [subsection entry parsing logic] ...
        stream.seek(-1, 1)
        trailer_tag = stream.read(7)
        if trailer_tag != b"trailer":
            # more xrefs!
            # FLAW: Relative seek backward without verifying forward progress
            # or checking how many bytes stream.read(7) actually consumed.
            stream.seek(-7, 1)
        else:
            break

The patch introduces a state tracking variable, previous_position: Optional[int] = None, initialized before the while True loop. At the end of each iteration, after rewinding the stream via stream.seek(-7, 1), the parser queries the current stream offset using stream.tell() and compares it against previous_position:

File: pypdf/_reader.py
@@ -1072,6 +1072,7 @@ def _read_standard_xref_table(self, stream: StreamType) -> None:
         stream.seek(-1, 1)
         first_time = True  # check if the first time looking at the xref table
         recovery_cache: Optional[dict[int, tuple[int, int]]] = None
+        previous_position: Optional[int] = None
         while True:
             num = cast(int, read_object(stream, self))
             if first_time and num != 0:
@@ -1186,12 +1187,20 @@ def _read_standard_xref_table(self, stream: StreamType) -> None:
                 stream.seek(-1, 1)
             stream.seek(-1, 1)
             trailer_tag = stream.read(7)
-            if trailer_tag != b"trailer":
-                # more xrefs!
-                stream.seek(-7, 1)
-            else:
+            if trailer_tag == b"trailer":
                 break
 
+            # More xrefs should move the parser to another section.
+            stream.seek(-7, 1)
+
+            # Detect a repeated read at the same position.
+            rewound_position = stream.tell()
+            if rewound_position == previous_position:
+                raise PdfReadError(
+                    f"Xref parsing stuck at position {rewound_position}: {trailer_tag!r}"
+                )
+            previous_position = rewound_position
+
     def _read_xref_tables_and_trailers(
         self, stream: StreamType, startxref: Optional[int], xref_issue_nr: int
     ) -> None:

> [!NOTE] > Assessment of Fix Completeness: Tracking previous_position directly detects single-step stationary stalls where rewound_position == previous_position across consecutive iterations, raising a PdfReadError. Because valid cross-reference subsections must advance the stream pointer forward as entries are consumed, comparing rewound_position against previous_position stops stationary EOF loops. A still more defensive invariant would enforce monotonic forward progress (rewound_position <= previous_position), ensuring that even multi-step oscillating seeks or backward jumps across corrupted tokens cannot cycle between two distinct offsets.

Exploitation

Exploiting CVE-2026-119203 requires no authentication or complex environmental conditions. The attacker only needs to deliver a crafted PDF file to a target application or service that invokes pypdf.PdfReader on the input stream. Common attack vectors include web application file upload endpoints (such as resume parsers, invoice processors, or document management platforms), email attachment scanners, and automated document indexing pipelines.

To construct a minimal trigger payload, the input stream only needs to satisfy the basic entry criteria of PdfReader: a valid %PDF- header, a startxref pointer directing the reader to the byte offset of an xref keyword, an xref subsection header specifying 0 object entries (e.g., 1 0), and trailing non-trailer bytes at the end of the file.

The official regression test included in commit 06593f993789a4b573dd955730c1a7051128bb2a provides a complete, 40-byte proof-of-concept payload:

from io import BytesIO
import pytest
from pypdf import PdfReader
from pypdf.errors import PdfReadError
 
def test_read_standard_xref_table__end_of_file__trailer():
    # 40-byte crafted PDF stream where the standard xref table
    # declares 0 entries ('1 0') and terminates with 'aaa' at EOF:
    data = b"%PDF-1.4\nstartxref\n28\n%%EOF\nxref\n1 0\naaa"
 
    # On pypdf < 6.21.4, calling PdfReader(BytesIO(data)) never returns.
    # On pypdf >= 6.21.4, PdfReadError is raised on the second iteration:
    with pytest.raises(PdfReadError, match=r"^Xref parsing stuck at position 33: b'aaa'$"):
        PdfReader(BytesIO(data))

When PdfReader(BytesIO(data)) is executed on pypdf 6.21.3 or earlier:

  1. The parser reads startxref at offset 28, which points to xref\n1 0\naaa.
  2. Inside _read_standard_xref_table, read_object reads the subsection start ID (1) and entry count (0).
  3. Because the entry count is 0, the inner entry loop completes immediately.
  4. The parser seeks back 1 byte and reads up to 7 bytes (trailer_tag), obtaining b"aaa" before hitting EOF at byte 40.
  5. Because b"aaa" != b"trailer", stream.seek(-7, 1) rewinds 7 bytes from offset 40 to offset 33 (1 0\naaa).
  6. The while True loop repeats from offset 33, reading 1 and 0 again and rewinding to offset 33 indefinitely.

Impact Assessment

The primary impact of CVE-2026-119203 is a high-severity Denial of Service (DoS) against the application availability, reflected in its CVSS v4.0 score of 7.1 (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). The vulnerability does not allow memory corruption, arbitrary code execution, or unauthorized data disclosure (VC:N/VI:N), as the operations are strictly confined to stream offset manipulation within the Python interpreter.

In synchronous Python web frameworks (such as Flask, Django, or FastAPI running synchronous worker pools via Gunicorn or uWSGI), each incoming HTTP request is handled by a dedicated worker process or thread. Because the infinite loop in _read_standard_xref_table is entirely CPU-bound (performing in-memory stream reads and seeks when backed by BytesIO or buffered file streams), a single malicious upload permanently occupies one worker thread and saturates a CPU core due to Python's Global Interpreter Lock (GIL) within that process.

By submitting multiple concurrent requests containing the 40-byte payload, an unauthenticated attacker can exhaust all available worker processes in the application pool. Once all workers are trapped in the while True loop, the service becomes completely unresponsive to legitimate users until the application processes are forcibly terminated and restarted by an external supervisor.

Remediation

The definitive remediation for CVE-2026-119203 is to upgrade pypdf to version 6.21.4 or later. In version 6.21.4, encountering an non-advancing stream offset during standard cross-reference table parsing immediately raises a pypdf.errors.PdfReadError exception, which calling applications can catch and handle cleanly.

Administrators and developers should update their dependencies using pip or their respective package manager:

pip install --upgrade "pypdf>=6.21.4"

When catching exceptions around PdfReader initialization in application code, ensure that PdfReadError (or its base class PyPdfError) is caught so that malformed uploads return a standard HTTP 400 Bad Request response rather than an unhandled 500 Internal Server Error:

from io import BytesIO
from pypdf import PdfReader
from pypdf.errors import PyPdfError
 
def parse_uploaded_pdf(raw_bytes: bytes) -> int:
    try:
        reader = PdfReader(BytesIO(raw_bytes))
        return len(reader.pages)
    except PyPdfError as exc:
        # Log and reject malformed or truncated PDF files safely
        raise ValueError("Invalid PDF document supplied") from exc

As a defense-in-depth measure for applications that must process untrusted PDF files, document parsing should be offloaded from web request threads into isolated background worker processes (such as Celery, RQ, or Python multiprocessing) configured with strict wall-clock and CPU time limits (for example, Gunicorn --timeout or Celery time_limit). Enforcing execution timeouts ensures that any future parser stalls or resource-exhaustion bugs in third-party file format libraries are terminated before exhausting host resources.

Official Patches

py-pdfOfficial GitHub Security Advisory (GHSA-5849-834g-hvw5)
py-pdfpypdf 6.21.4 Release Notes
py-pdfPull Request #4289: SEC: Detect EOF when reading standard xref table trailer

Fix Analysis (1)

Technical Appendix

CVSS Score
7.1/ 10
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
EPSS Probability
0.30%
Top 77% most exploited

Affected Systems

pypdf (PyPI package) < 6.21.4

Affected Versions Detail

Product
Affected Versions
Fixed Version
pypdf
py-pdf
< 6.21.46.21.4
AttributeDetail
Vulnerability IDCVE-2026-119203 / GHSA-5849-834g-hvw5
Weakness ClassCWE-835: Loop with Unreachable Exit Condition ('Infinite Loop')
CVSS v4.0 Score7.1 (High) - 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
Affected Componentpypdf._reader.PdfReader._read_standard_xref_table (< 6.21.4)
Patched Versionpypdf 6.21.4
Primary ImpactDenial of Service via CPU and worker thread exhaustion
EPSS Score0.30% (23.18th Percentile)
CISA KEV StatusNot Listed

MITRE ATT&CK Mapping

T1499Endpoint Denial of Service
Impact
T1499.003Application Exhaustion Flood
Impact
CWE-835
Loop with Unreachable Exit Condition ('Infinite Loop')

The program contains an iteration or loop statement whose exit condition cannot be satisfied, causing the loop to repeat indefinitely.

Known Exploits & Detection

GitHubOfficial regression test case in the security fix commit demonstrating a 40-byte PDF stream that triggers the infinite loop in pypdf < 6.21.4.

Vulnerability Timeline

Security fix committed in Pull Request #4289 (commit 06593f993789a4b573dd955730c1a7051128bb2a) and version 6.21.4 released.
2026-10-28
GitHub Security Advisory GHSA-5849-834g-hvw5 and CVE-2026-119203 published to NVD.
2026-10-29
GitHub Advisory record updated with final metadata.
2026-10-30

References & Sources

  • [1]GitHub Security Advisory GHSA-5849-834g-hvw5
  • [2]Pull Request #4289: SEC: Detect EOF when reading standard xref table trailer
  • [3]Official Security Fix Commit (06593f993789a4b573dd955730c1a7051128bb2a)
  • [4]pypdf Release 6.21.4
  • [5]CVE.org Record: CVE-2026-119203
  • [6]NVD Vulnerability Detail: CVE-2026-119203
  • [7]cvelistV5 JSON Entry for CVE-2026-119203

Attack Flow Diagram

Press enter or space to select a node. You can then use the arrow keys to move the node around. Press delete to remove it and escape to cancel.
Press enter or space to select an edge. You can then press delete to remove it or escape to cancel.

More Reports

•24 minutes ago•CVE-2026-119204
7.1

CVE-2026-119204: Uncontrolled Resource Consumption in pypdf Page Tree Flattening

CVE-2026-119204 (also tracked as GHSA-cggf-277g-3q54) is a Denial of Service (DoS) vulnerability in pypdf prior to version 6.21.4. The vulnerability stems from uncontrolled resource consumption (CWE-400) during the flattening of a PDF document's page tree. By supplying a compact PDF containing a hierarchical amplification structure of shared `/Pages` and `/Page` nodes with dense dictionary entries or inherited resources, an unauthenticated attacker can cause pypdf to copy millions of dictionary entries into individual `PageObject` instances without enforcing a document-wide entry budget. This triggers severe heap memory exhaustion and process termination via Out-Of-Memory (OOM) kills.

Alon Barad
Alon Barad
1 views•11 min read
•about 2 hours ago•CVE-2026-118759
7.5

CVE-2026-118759: Cross-Tenant Pod Log Exposure via Authorization Bypass in Kubeflow Pipelines

Prior to version 2.18.0, the Kubeflow Pipelines API server contains a missing authorization and confused deputy vulnerability in its run log streaming endpoint. The HTTP handler for `GET /apis/v1alpha1/runs/{run_id}/nodes/{node_id}/log` fails to propagate incoming request headers to gRPC metadata and omits caller authorization checks against the requested run. Furthermore, the backend accepts the caller-supplied `node_id` parameter directly as a Kubernetes pod name without verifying that the pod belongs to the specified pipeline run. An attacker with network access to the API server who knows a valid run identifier can read live or archived execution logs from any pod containing a `main` container within the run's Kubernetes namespace.

Alon Barad
Alon Barad
4 views•10 min read
•about 3 hours ago•CVE-2026-118761
7.5

CVE-2026-118761: Pre-Authorization Decompression Bomb Denial of Service in Kubeflow Pipelines API Server

A high-severity Denial of Service (DoS) vulnerability exists in Kubeflow Pipelines prior to version 2.18.0 due to improper handling of highly compressed archive files (CWE-409). The API server decompresses uploaded ZIP and gzip-compressed TAR archives directly into memory without bounding the expanded byte stream or archive traversal metadata. Because decompression occurs before namespace-level authorization in multipart upload handlers, an unauthenticated or unauthorized attacker with network access to the API server can exhaust container heap memory and trigger Out-Of-Memory (OOM) terminations.

Amit Schendel
Amit Schendel
4 views•11 min read
•about 4 hours ago•CVE-2026-118763
7.4

CVE-2026-118763: Cross-Origin Credential Exposure in @sigstore/oci

A credential exposure vulnerability exists in the @sigstore/oci library prior to version 0.7.2. The RegistryClient implementation stores sensitive authentication headers, including Bearer tokens, Basic authentication credentials, and custom Docker configuration headers, as client-wide defaults on its shared HTTP client instance. When uploading binary blobs via the OCI distribution specification, the client extracts the target upload URI from the server-supplied Location header and dispatches an independent HTTP PUT request. If a compromised or untrusted registry returns a cross-origin URI in the Location header, the client transmits the stored authentication credentials directly to the third-party host.

Alon Barad
Alon Barad
7 views•10 min read
•about 7 hours ago•CVE-2026-119282
7.3

CVE-2026-119282: OS Command Injection via Plugin Options in Nx (@nx/maven and @nx/gradle)

An OS command injection and argument injection vulnerability exists in the Nx build system and its official Maven (@nx/maven) and Gradle (@nx/gradle) plugins. When constructing the project dependency graph, the plugins invoke underlying Java build wrappers via subshells while interpolating unsanitized plugin options directly from nx.json into the command arguments. An attacker who supplies a crafted nx.json file via a repository or pull request can execute arbitrary shell commands with the privileges of the user or CI/CD runner invoking any Nx command that computes the project graph.

Amit Schendel
Amit Schendel
8 views•10 min read
•about 8 hours ago•CVE-2026-59978
7.5

CVE-2026-59978: Persistent Denial of Service via Array Injection in AdminNeo

AdminNeo versions 4.6.2 through 5.4.1 contain an improper input validation vulnerability (CWE-20) in the version verification endpoint (?script=version). An unauthenticated remote attacker can submit a crafted HTTP POST request containing an array parameter for 'version', which is serialized into a shared server-side temporary cache file without type checking or authentication. On subsequent requests, the application deserializes this cached array and passes it to cookie() and rawurlencode(), triggering an uncaught fatal TypeError in PHP 8.x environments and causing a persistent 24-hour Denial of Service (DoS) for all users.

Amit Schendel
Amit Schendel
6 views•8 min read