GHSA-m8gf-v64p-gfmg BabelDOC: Arbitrary Code Execution via CMap Pickle Deserialization in babeldoc/pdfminer/cmapdb.py
Arbitrary Code Execution via CMap Pickle Deserialization in babeldoc/pdfminer/cmapdb.pySummaryBabelDOC'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.DetailsThe vulnerable function is `CMapDB._load_data()` at `babeldoc/pdfminer/cmapdb.py:232–245`:**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:**`babeldoc/main.py:611–622` — CLI accepts a PDF path; only existence and `.pdf` suffix are checked.`babeldoc/main.py:678–679` — path stored in `TranslationConfig(input_file=file)`.`babeldoc/format/pdf/high_level.py:472–488` — `translation_config.input_file` enters the translate pipeline.`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`.`babeldoc/format/pdf/new_parser/native_parse.py:60–70` — prepared pages loaded and interpreted.`babeldoc/format/pdf/new_parser/pymupdf_prepared_page_access.py:25–34` — PyMuPDF opens the PDF and builds page resources.`babeldoc/format/pdf/new_parser/prepared_resource_builder.py:84–94` — font resources converted to `PreparedFontSpec`.`babeldoc/format/pdf/new_parser/active_font_resource_runtime.py:21–35` — page resource bundle resolves root font map.`babeldoc/format/pdf/new_parser/active_font_runtime.py:79–87` — each font spec projected and passed to `font_factory.create_font`.`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)`.`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.`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`):A more complete fix replaces the pickle-backed CMap loader with a signed or static data format (e.g., JSON or generated Python modules) that does not carry executable code.PoC**Environment setup (Docker — recommended for isolation):****Manual setup (local venv):****PoC script (`poc.py`) — key steps:****Phase 2 dynamic reproduction output (Docker container):**The `TypeError` is benign and expected: `write_text()` returns an integer, and the subsequent `type(name, (), <int>)` call in `_load_data()` raises before reaching further code. The payload already executed successfully at that point.**Attack path summary:**ImpactThis is an **Arbitrary Code Execution** vulnerability triggered by processing a crafted PDF file. Any user or automated pipeline that runs BabelDOC against untrusted PDF input is at risk.**Who is impacted:****End users** who open a malicious PDF with the `babeldoc` CLI or any application embedding BabelDOC's PDF translation/text-extraction functionality.**Automated document processing pipelines** (CI translation services, document management systems, cloud PDF processors) that ingest user-supplied PDFs without sandboxing.**Attack prerequisites:**The attacker must be able to place a `.pickle.gz` file at a predictable path on the local filesystem (e.g., `/tmp/`), or exploit a shared world-writable directory. On Windows systems, UNC/WebDAV paths may provide a remote staging alternative.The victim must process the crafted PDF through BabelDOC. No elevated privileges or special configuration is required — default PDF processing is the vulnerable code path.**Scope:** The attack crosses security boundaries (e.g., a lower-privileged attacker influencing files processed by a different user's process), justifying the **Changed** scope in the CVSS vector and potential lateral movement between users on multi-user systems.**Consequences:** Full code execution with the victim process's privileges — confidentiality breach, data modification, denial of service, and potential privilege escalation depending on the deployment context.Reproduction artifacts`Dockerfile``poc.py`Notes from the maintainerCVSS revision noteThe CVSS v3.1 vector has been revised from the reporter's initial
`CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H` (8.6) to
`CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H` (7.8) on maintainer
review. The severity rating remains **High**.One metric is revised; the remaining metrics (`AV:L`, `AC:L`, `PR:N`,
`UI:R`, `C:H/I:H/A:H`) are unchanged from the reporter's assessment.**Scope: Changed → Unchanged.** BabelDOC is a PDF-processing
library running with the caller process's operating-system
permissions; it does not enforce a separate security authority over
OS files, users, or downstream services. The malicious pickle
payload executes in that same BabelDOC Python process. Under CVSS
v3.1, this is Scope Unchanged: the vulnerable component and the
impacted component are governed by the same authority. No sandbox,
VM, browser-client, or application-defined authorization boundary
is crossed.The remaining metrics are retained intentionally:`AV:L`, `PR:N`, `UI:R`: the attack requires local presence of
attacker-influenced data (consistent with `AV:L`), does not require
authenticated access to BabelDOC itself (`PR:N`), and depends on a
user actually processing the crafted PDF (`UI:R`).`AC:L`: kept aligned with industry practice for CWE-502
deserialization issues; once the supporting filesystem condition
exists, the same-process exploitation path is consistent and
repeatable.`C:H`, `I:H`, `A:H`: full code-execution impact within the
BabelDOC process.We thank EQSTLab for the detailed report and PoC; this revision is
limited to CVSS metric interpretation, and the issue remains High
severity when exploitable.Full sink coverage (2 independently exploitable PDF paths + 2 defense-in-depth call sites)The original report covers entry point (1): the `Encoding` / `CMapName`
font dictionary path, with absolute-path injection. Local review during
patch preparation identified that the same `_load_data` sink is reached
from one additional independently exploitable PDF-controlled path and
two prefixed call sites covered at the sink for defense in depth:`Encoding` / `CMapName` references in a font dictionary
*(reported entry; absolute-path injection per the upstream report,
`..` relative traversal also exploitable)*The PostScript `usecmap` operator inside an embedded CMap stream
*(independently exploitable via `..` relative traversal; not in the
original report)*`CIDSystemInfo.Ordering` flowing through `get_unicode_map` in the
legacy pdfminer pipeline`CIDSystemInfo.Ordering` flowing through `get_unicode_map` in the
active new-parser pipelineCall sites (3) and (4) were not reproduced as standalone PDF-only
exploit paths in v0.6.x. The `get_unicode_map` caller prepends a
`to-unicode-` prefix to the PDF-controlled name, which breaks
absolute-path injection and means `..` traversal would require an
additional crafted directory layout such as a `to-unicode-*`
component under a CMap search location. The 0.6.3 sink-level fix
still covers these call sites, so future removal of the prefix or
a future unprefixed caller remains blocked.Fix designThe runtime CMap loader in 0.6.3 refuses to deserialize any file that
does not simultaneously:appear in a pinned manifest of bundled CMap filenames (allowlist),resolve inside the bundled `runtime/data/cmap` directory after path
resolution (containment check), andbyte-for-byte match the manifest's pinned byte size and SHA-256.The integrity check runs on the compressed on-disk `.gz` bytes before
decompression, so files whose compressed size or SHA-256 differs from
the pinned manifest are rejected before `gzip` or `pickle` sees them.
The legacy `CMAP_PATH` external search path is removed entirely; only
the bundled directory is consulted. The active new-parser pipeline
and the vendored pdfminer pipeline share the same verified-load entry
point.Related hardening shipped in 0.6.3A separate hardening in the same release sanitizes PDF-controlled
XObject names before they reach the optional `ImageWriter` output
path, preventing PDF-driven writes outside the configured output
directory. This is separate from BabelDOC's default translation
pipeline: the optional `ImageWriter` is not used by default and is
only reachable when a third-party caller passes an explicit
`output_dir`. It is included here for completeness.Risk reduction if you cannot upgrade immediatelyThese steps reduce known exploit preconditions on pre-0.6.3 versions;
they are not equivalent to the 0.6.3 fix.Do not set the `CMAP_PATH` environment variable when running
BabelDOC. 0.6.3 removes this variable entirely; on pre-0.6.3
versions, unsetting it limits the attack surface to the bundled
cmap directory under the BabelDOC package.Run BabelDOC under an account that cannot create files in any
directory BabelDOC will read CMap data from, including any
pre-0.6.3 `CMAP_PATH` target.Process only PDFs from trusted sources until upgrading.Maintenance policyBabelDOC publishes security fixes only in the latest release. We do
not publish maintainer-supported backports for older minor, patch, or
release lines. For this advisory, the maintainer-supported fixed
version is 0.6.3 or later; downstream distributors may carry their
own patches, but older BabelDOC releases will not receive a separate
upstream backport.AcknowledgementsWe thank **EQSTLab** for the detailed private report, complete
reproduction material, and coordinated-disclosure cooperation that
allowed this fix to be prepared and released before public
disclosure.Timeline2026-06-03 04:34 UTC: EQSTLab opens the private advisory draft and
notifies maintainers2026-06-03 09:21 UTC: BabelDOC 0.6.3 released with the fix2026-06-03 09:50 UTC: this advisory publishedTBD: CVE identifier assigned (pending GitHub CNA review; GitHub
documentation says CVE requests are usually reviewed within 72
hours)ReferencesBabelDOC 0.6.3 release notes:
https://github.com/funstory-ai/BabelDOC/blob/main/docs/release-notes/v0.6.3.mdCVE: TBD (pending CNA assignment)
CVSS v3.1 score: 7.8