pypdf: Incomplete Fix of CVE-2026-27025 — /ToUnicode CMap bfrange Per-Entry Byte-Width Is Still Unbounded
| Field | Value |
|---|---|
| Package | pypdf |
| Version | <= 6.14.2 (latest at time of analysis) |
| Repository | https://github.com/py-pdf/pypdf |
| Weekly Downloads | ~20M (PyPI) |
| Vulnerability Type | CWE-400: Uncontrolled Resource Consumption / CWE-789: Memory Allocation with Excessive Size Value |
| CVSS Score | 6.5 (Medium) — AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H |
| Impact | Memory exhaustion / OOM during extract_text() on an attacker-supplied PDF |
| Prior art | Incomplete fix of CVE-2026-27025 / GHSA-wgvp-vg3v-2xq3 (fixed in 6.7.1, PR #3646) |
| Candidate | IDI-CAND-0047-pypdf/IDI-2026-0017 |
This is a security regression / incomplete fix of CVE-2026-27025
(GHSA-wgvp-vg3v-2xq3), which was addressed in pypdf 6.7.1 by PR #3646 "Limit size of
/ToUnicode entries". That patch introduced MAPPING_DICTIONARY_SIZE_LIMIT = 100_000, which
caps the number of /ToUnicode CMap entries but does not cap the byte width of an
individual entry. Because the hex token length inside a beginbfrange … endbfrange block
determines the key width stored in map_dict, an attacker can keep the entry count under the
100,000 limit while inflating each entry's byte width with leading-zero padding. The product
(100,000 entries × unbounded per-entry width) drives memory growth that the count cap does not
constrain. The condition is reached through the ordinary extract_text() path — the same
function, same threat model, and same advisory text ("unusually large values … large memory
consumption during text extraction") as CVE-2026-27025.
We report this as a follow-on robustness/regression fix rather than a novel discovery, and note that the original advisory's wording is broad enough that the CNA may choose to fold this into the existing advisory rather than assign a new CVE.
pypdf/_cmap.py, parse_bfrange():
# _check_mapping_size() enforces the entry COUNT cap from #3646 …
def _check_mapping_size(map_dict, ...):
if len(map_dict) > MAPPING_DICTIONARY_SIZE_LIMIT: # 100_000 — count only
raise ...
# … but the per-entry key WIDTH is taken straight from the token length:
nbi = max(len(lst[0]), len(lst[1])) # token byte length, no upper bound
map_dict[-1] = ceil(nbi / 2) # key width derived from attacker token length#3646 added the count limit (MAPPING_DICTIONARY_SIZE_LIMIT) but left the per-entry byte
width (nbi, derived directly from the hex token length) unbounded. Leading-zero padding in the
bfrange hex tokens lets the attacker grow nbi arbitrarily while each padded token still
counts as a single entry, so the count cap never trips.
Why the OOM is not caught: process_cm_line only catches (ValueError, IndexError), and
the extract_text() call site in _page.py only catches (AttributeError, TypeError). A
MemoryError — or an OS OOM-killer terminating the process for a large cumulative allocation —
propagates and crashes the consumer.
Confirmed by code trace, default extract_text() path:
PdfReader.pages[i].extract_text()
→ _page.py from_font_resource()
→ _font.py get_encoding()
→ _cmap.py _parse_to_unicode() → parse_bfrange()
A /ToUnicode CMap is a standard component of Type0/CID fonts, so it is always processed on the
normal text-extraction path. No special configuration, object injection, network, or elevated
privilege is required — the single precondition is "call extract_text() on an untrusted PDF."
- A service extracts text from user-supplied PDFs (uploads, mail attachments, crawled
documents) using
pypdf— pypdf's headline feature, routinely applied to untrusted input. - The attacker crafts a small PDF embedding a Type0 font whose
/ToUnicodestream contains abeginbfrange … endbfrangeblock with leading-zero-padded hex tokens. - On
extract_text(),parse_bfrangeallocates ~100,000 entries each of attacker-controlled byte width, exhausting memory and crashing the worker / process.
- Availability: High — memory exhaustion / OOM crashes the processing worker or host.
- Confidentiality / Integrity: None — pure resource-consumption DoS; no code execution or data disclosure.
Measured amplification: a 2,011-byte input produced a 121 MB peak (×60,163). Scaling the padded token toward the existing 75 MB stream ceiling drives the peak into the tens of GB, reaching OOM on typical worker memory limits.
poc.py builds a minimal single-page PDF with a Type0 font and a malicious /ToUnicode CMap,
then drives the full extract_text() path (not a unit-level call to parse_bfrange) and
reports peak memory via tracemalloc/resource.
pip install pypdf==6.14.2
python3 poc.pyExpected: a small input PDF yields a multi-x peak-memory amplification during
reader.pages[0].extract_text(); raising the padding width pushes the process to OOM.
Decision is the maintainer's; only direction is offered. Bound the per-entry key byte width in
parse_bfrange (cap nbi / map_dict[-1]), or extend _check_mapping_size to account for
cumulative bytes rather than entry count alone, so the #3646 count cap is complemented by
a width/total-size cap. This closes the residual gap left by the CVE-2026-27025 fix.
- CVE-2026-27025 / GHSA-wgvp-vg3v-2xq3 — pypdf
/ToUnicodelarge-value memory consumption during text extraction, fixed in 6.7.1 (PR #3646 "Limit size of /ToUnicode entries"). This report is the residual incomplete fix of that advisory. - CVE-2023-36464 (IndexError), CVE-2023-36810 (infinite loop) — prior pypdf DoS precedents (different root cause; cited for class acceptance of CWE-400 in PDF parsers).
- CWE-400: Uncontrolled Resource Consumption
- CWE-789: Memory Allocation with Excessive Size Value
- 2026-06-29: Regression identified and reproduced on pypdf 6.14.2 (latest) via the full
extract_text()path; confirmed as the residual byte-width gap left by PR #3646.