{"repro_id":"REPRO-2026-00080","version":3,"title":"Docling-core YAML Deserialization RCE via FullLoader","repro_type":"security","status":"published","severity":"high","description":"A PyYAML-related Remote Code Execution (RCE) vulnerability is exposed in docling-core >=2.21.0, <2.48.4 when the application uses pyyaml < 5.4 and invokes DoclingDocument.load_from_yaml() with untrusted YAML data. The unsafe yaml.FullLoader allows attacker-controlled Python object construction, leading to arbitrary command execution during deserialization before any validation occurs.","root_cause":"## Summary\n`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.\n\n## Impact\n- **Component:** `docling_core.types.doc.DoclingDocument.load_from_yaml`\n- **Affected versions:** docling-core >= 2.21.0, < 2.48.4 when used with PyYAML < 5.4\n- **Risk level:** High — arbitrary command execution when parsing untrusted YAML\n- **Consequence:** An attacker can execute OS commands during YAML deserialization even if the resulting object fails validation.\n\n## Root Cause\n`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.\n\n## Reproduction Steps\n1. Run `repro/reproduction_steps.sh`.\n2. 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`.\n3. Evidence of reproduction is the creation of `logs/pwned.txt` containing the output of `id`.\n\n## Evidence\n- **Log/artifact:** `logs/pwned.txt`\n- **Key output (from script):**\n  - `VULNERABILITY CONFIRMED: marker file created at .../logs/pwned.txt`\n  - Script prints a validation error after deserialization, demonstrating the payload executes before validation.\n- **Environment:** Python 3.12 venv with docling-core 2.48.3 and PyYAML 5.3.1\n\n## Recommendations / Next Steps\n- Upgrade to docling-core 2.48.4 or later, which uses `yaml.SafeLoader`.\n- If upgrading is not possible, explicitly use `yaml.safe_load` or `SafeLoader` when parsing untrusted YAML.\n- Add regression tests that feed malicious YAML payloads into `load_from_yaml` to ensure unsafe tags are rejected.\n\n## Additional Notes\n- The reproduction script is idempotent and can be run multiple times; it overwrites the payload and marker file on each run.\n- Even though the YAML fails `DoclingDocument` validation, the exploit triggers during deserialization, so validation alone is insufficient protection.\n","ghsa_id":"GHSA-VQXF-V2GG-X3HC","cve_id":"CVE-2026-24009","package":{"name":"docling-core","ecosystem":"pip","affected_versions":">= 2.21.0, < 2.48.4","fixed_version":"2.48.4"},"reproduced_at":"2026-02-13T13:17:05.775085+00:00","duration_secs":361.0,"tool_calls":58,"handoffs":2,"quality":{"idempotent_verified":false,"community_verifications":0},"published_at":"2026-02-13T13:17:06.311203+00:00","retracted":false,"artifacts":[{"path":"repro/rca_report.md","filename":"rca_report.md","size":2642,"category":"analysis"},{"path":"repro/reproduction_steps.sh","filename":"reproduction_steps.sh","size":974,"category":"reproduction_script"},{"path":"vuln_variant/rca_report.md","filename":"rca_report.md","size":3528,"category":"analysis"},{"path":"vuln_variant/reproduction_steps.sh","filename":"reproduction_steps.sh","size":2846,"category":"reproduction_script"},{"path":"bundle/ticket.md","filename":"ticket.md","size":2644,"category":"ticket"},{"path":"logs/pwned.txt","filename":"pwned.txt","size":54,"category":"other"},{"path":"logs/payload.yaml","filename":"payload.yaml","size":184,"category":"other"},{"path":"vuln_variant/patch_analysis.md","filename":"patch_analysis.md","size":1114,"category":"documentation"},{"path":"logs/vulnerable_attempt_1.log","filename":"vulnerable_attempt_1.log","size":240,"category":"log"},{"path":"logs/vulnerable_attempt_2.yaml","filename":"vulnerable_attempt_2.yaml","size":205,"category":"other"},{"path":"logs/vulnerable_attempt_3.yaml","filename":"vulnerable_attempt_3.yaml","size":164,"category":"other"},{"path":"logs/vulnerable_summary.log","filename":"vulnerable_summary.log","size":362,"category":"log"},{"path":"logs/vulnerable_attempt_1.yaml","filename":"vulnerable_attempt_1.yaml","size":131,"category":"other"},{"path":"logs/fixed_attempt_3.log","filename":"fixed_attempt_3.log","size":249,"category":"log"},{"path":"logs/vulnerable_attempt_3.log","filename":"vulnerable_attempt_3.log","size":254,"category":"log"},{"path":"logs/fixed_attempt_1.log","filename":"fixed_attempt_1.log","size":235,"category":"log"},{"path":"logs/fixed_summary.log","filename":"fixed_summary.log","size":236,"category":"log"},{"path":"logs/fixed_attempt_1.yaml","filename":"fixed_attempt_1.yaml","size":126,"category":"other"},{"path":"logs/bypass_result.log","filename":"bypass_result.log","size":59,"category":"log"},{"path":"logs/vulnerable_attempt_2.log","filename":"vulnerable_attempt_2.log","size":275,"category":"log"},{"path":"logs/fixed_attempt_2.yaml","filename":"fixed_attempt_2.yaml","size":200,"category":"other"},{"path":"logs/fixed_attempt_2.log","filename":"fixed_attempt_2.log","size":229,"category":"log"},{"path":"logs/fixed_attempt_3.yaml","filename":"fixed_attempt_3.yaml","size":159,"category":"other"},{"path":"logs/vulnerable_attempt_2_pwned.txt","filename":"vulnerable_attempt_2_pwned.txt","size":54,"category":"other"}]}