Skip to main content
VulnerabilityPublished

CVE-2026-54071: Arbitrary Code Execution via CMap Pickle Deserialization in babeldoc/pdfminer/cmapdb.py

Affects BabelDOCSource GCVE Database

0
Compare
Authoritative record:CVE-2026-54071· High 7.8

Not known to be exploited · CVSS 7.8 High : upgrade to 0.6.3 soon.

Data as of 29 Oct 2026, 19:04 UTC

CVSS 3.1 base score
7.8High
How severe the flaw is on the 0–10 CVSS scale.
EPSS
0.3% chance of exploitation in 30 days – higher than 31% of CVEsExploit Prediction Scoring System: the likelihood attackers will use this flaw soon.
Patch
Available – fixed in 0.6.3
CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H

Exploitation evidence

  1. Disclosed: evidence found.

    CVE published 21 Aug 2026

  2. Detectable: no evidence on record.

    No public nuclei template indexed

  3. Weaponized: no evidence on record.

    No public exploit code indexed in Metasploit or Exploit-DB

  4. Exploited in the wild: no evidence on record.

    Not in CISA KEV and no source reports exploitation

  5. Observed by OffSeq Mirage: no evidence on record.

    No attack traffic recorded by OffSeq Mirage sensors

A missing step is not evidence of safety. Sources: cve.org (opens in a new tab), the Metasploit, Exploit-DB and nuclei-templates index as of 29 Oct 2026.

What to do

Upgrade BabelDOC to 0.6.3.

Affected software

ProductAffectedFixed in
PyPIBabelDOCAffected
  • <0.6.3
Fixed in
  • 0.6.3

Check your own hosts for these packages with threat-finder (opens in a new tab), our free open-source scanner.

Description

## Arbitrary Code Execution via CMap Pickle Deserialization in babeldoc/pdfminer/cmapdb.py ### Summary BabelDOC's vendored PDF parser (`babeldoc/pdfminer/cmapdb.py`) deserializes untrusted pickle data when loading CMap files. The `_load_data()` method strips only NUL bytes from a PDF-controlled CMap name, then passes it directly to `os.path.join()` and `pickle.loads()`. Because Python's `os.path.join()` discards all preceding path components when it encounters an absolute path segment, an attacker who embeds a hex-encoded absolute path in a crafted PDF's `/Encoding` name (e.g., `/#2Ftmp#2Fattacker#2Fevil`) can redirect deserialization to any attacker-writable `.pickle.gz` file on the local system. Processing such a PDF results in arbitrary Python code execution with the privileges of the BabelDOC process. ### Details The vulnerable function is `CMapDB._load_data()` at `babeldoc/pdfminer/cmapdb.py:232–245`: ```python @classmethod def _load_data(cls, name: str) -> Any: name = name.replace("\0", "") # line 233 — only NUL is stripped filename = "%s.pickle.gz" % name # line 234 — attacker-controlled string ... for directory in cmap_paths: path = os.path.join(directory, filename) # line 241 — no realpath/canonical check if os.path.exists(path): gzfile = gzip.open(path) try: return type(str(name), (), pickle.loads(gzfile.read())) # line 245 — unconditional pickle ``` **Path injection via PDF name hex-encoding.** The PDF specification allows name objects to encode arbitrary bytes as `#xx`. The pdfminer literal-name parser (`psparser._parse_literal_hex`) decodes these sequences before handing the string to higher layers. Consequently, the PDF literal `/#2Ftmp#2Fattacker#2Fevil` is decoded to the Python string `/tmp/attacker/evil`. **Python `os.path.join()` absolute-path override.** When the decoded name starts with `/` (i.e., it is an absolute path), Python's `os.path.join(directory, name + ".pickle.gz")` ignores `directory` entirely and returns the absolute path unchanged. The trusted `cmap_paths` directories (`/usr/share/pdfminer/`, the package's own `cmap/` folder) are therefore completely bypassed. **Data flow from PDF to sink:** 1. `babeldoc/main.py:611–622` — CLI accepts a PDF path; only existence and `.pdf` suffix are checked. 2. `babeldoc/main.py:678–679` — path stored in `TranslationConfig(input_file=file)`. 3. `babeldoc/format/pdf/high_level.py:472–488` — `translation_config.input_file` enters the translate pipeline. 4. `babeldoc/format/pdf/high_level.py:805–848` — PDF saved to `temp_pdf_path` and parsed with `parse_prepared_pdf_with_new_parser_to_legacy_ir`. 5. `babeldoc/format/pdf/new_parser/native_parse.py:60–70` — prepared pages loaded and interpreted. 6. `babeldoc/format/pdf/new_parser/pymupdf_prepared_page_access.py:25–34` — PyMuPDF opens the PDF and builds page resources. 7. `babeldoc/format/pdf/new_parser/prepared_resource_builder.py:84–94` — font resources converted to `PreparedFontSpec`. 8. `babeldoc/format/pdf/new_parser/active_font_resource_runtime.py:21–35` — page resource bundle resolves root font map. 9. `babeldoc/format/pdf/new_parser/active_font_runtime.py:79–87` — each font spec projected and passed to `font_factory.create_font`. 10. `babeldoc/format/pdf/new_parser/active_direct_font_backend.py:291–292, 491–493` — CID fonts call `build_cid_cmap(spec, literal_name=literal_name)`. 11. `babeldoc/format/pdf/new_parser/runtime/cid_cmap_runtime.py:52–77` — PDF-controlled `/Encoding`/`CMapName` normalized and passed to `CMapDB.get_cmap`. `_normalize_cmap_name()` removes only a single leading `/`; all other path characters pass through. 12. `babeldoc/pdfminer/cmapdb.py:233–245` — **sink**: NUL-stripped name used verbatim to construct the path; file opened with gzip and deserialized with `pickle.loads()`. **Sanitization gaps:** - `name.replace("\0", "")` removes only the NUL byte; `..`, `/`, `\`, and hex-decoded path separators are unaffected. - There is no `os.path.realpath()`, `os.path.abspath()`, or `os.path.commonpath()` containment check before the file is opened. - There is no allowlist of known CMap names nor any integrity verification of the pickle data. **Recommended patch** (`babeldoc/pdfminer/cmapdb.py`): ```diff --- a/babeldoc/pdfminer/cmapdb.py +++ b/babeldoc/pdfminer/cmapdb.py @@ cmap_paths = ( os.environ.get("CMAP_PATH", "/usr/share/pdfminer/"), os.path.join(os.path.dirname(__file__), "cmap"), ) for directory in cmap_paths: - path = os.path.join(directory, filename) + base_dir = os.path.realpath(directory) + path = os.path.realpath(os.path.join(base_dir, filename)) + try: + if os.path.commonpath([base_dir, path]) != base_dir: + continue + except ValueError: + continue if os.path.exists(path): gzfil

References

Community reviews

Loading reviews…