# REPRO-2026-00277: Apache Airflow DAG author RCE via unrestricted import_string() in BaseSerialization.deserialize() ## Summary Status: published Severity: critical Type: security Confidence: high ## Identifiers REPRO ID: REPRO-2026-00277 CVE: CVE-2026-33264 ## Package Name: apache/airflow Ecosystem: github Affected: < 3.3.0 Fixed: 3.3.0 ## Root Cause # CVE-2026-33264 — Root Cause Analysis ## Summary Apache Airflow prior to 3.3.0 allows a DAG author to execute arbitrary code on the Scheduler or API Server process during DAG deserialization. When the scheduler/API server loads serialized DAG content, `BaseSerialization.deserialize()` in `airflow/serialization/serialized_objects.py` unconditionally calls `import_string()` on attacker-controlled class paths embedded in serialized objects such as `BaseTrigger`. Importing an arbitrary Python module executes its module-level code, which crosses the trust boundary between DAG-author code and the scheduler/API server. Apache Airflow 3.3.0 removes this behavior and no longer deserializes `base_trigger` payloads through `BaseSerialization.deserialize()`. ## Impact - **Package/component affected:** Apache Airflow (`airflow.serialization.serialized_objects.BaseSerialization.deserialize()`) - **Affected versions:** All versions before 3.3.0 (reproduced against 3.2.2) - **Fixed versions:** 3.3.0 and later - **Risk level:** Critical - **Consequences:** A malicious DAG author can embed a class path that points to a module under their control. When the scheduler or API server deserializes the DAG, the module is imported and its top-level code executes in that process, leading to remote code execution in a trusted Airflow component. ## Impact Parity - **Disclosed/claimed maximum impact:** Remote code execution in the Scheduler/API Server via attacker-controlled class paths in serialized DAG content. - **Reproduced impact from this run:** Code execution demonstrated. A crafted `base_trigger` payload caused the vulnerable Airflow 3.2.2 installation to import an attacker-controlled module and execute its module-level code, which wrote a marker file (`PWNED`). The fixed 3.3.0 installation rejected the same payload with `TypeError: Invalid type base_trigger in deserialization.` and did not perform the import or write the marker. - **Parity:** `full` for the library-level claim. The harness directly invokes `BaseSerialization.deserialize()`, the same path the scheduler/API server uses when loading serialized DAGs, so the root cause and impact are matched. - **Not demonstrated:** No scheduler or API server process was actually started; the proof is a realistic harness that exercises the deserialization entry point directly. A full end-to-end DAG-file → scheduler parsing loop would further strengthen the claim but is not required for the submitted `library_api` surface. ## Root Cause In Airflow 3.2.2, `BaseSerialization.deserialize()` contains a branch that handles `DAT.BASE_TRIGGER`: ```python elif type_ == DAT.BASE_TRIGGER: tr_cls_name, kwargs = cls.deserialize(var) tr_cls = import_string(tr_cls_name) return tr_cls(**kwargs) ``` The `tr_cls_name` value is read directly from the serialized payload (`__var[0]`), and `import_string()` is called without any allowlist check. This means any importable class path supplied by the DAG author is imported during deserialization. Because Python module import executes top-level code, an attacker-controlled module runs in the scheduler/API server process. In Airflow 3.3.0, the `DAT.BASE_TRIGGER` branch has been removed from `BaseSerialization.deserialize()`, so the same payload falls through to the generic `else` branch and raises `TypeError: Invalid type base_trigger in deserialization.` before any import occurs. Trigger objects are now deserialized through a more restricted path that does not expose arbitrary `import_string()` behavior to DAG authors. ## Reproduction Steps 1. Run `bundle/repro/reproduction_steps.sh`. 2. The script detects the prepared project cache (`/data/pruva/project-cache/03610ec6-e6fb-4086-9681-103dd9199da6/repo`) and uses two pre-built virtualenvs: - `airflow_product_venv` — Apache Airflow 3.2.2 (vulnerable) - `airflow_3.3.0_cve_2026_33264_venv` — Apache Airflow 3.3.0 (fixed) 3. The script writes `bundle/repro/deserialize_harness.py`, which: - Creates a temporary attacker-controlled Python module (`evil_trigger.py`) whose top-level code writes a marker file (`PWNED`) when imported. - Builds a serialized `base_trigger` payload with `__classname__` pointing to `evil_trigger.EvilTrigger`. - Calls `BaseSerialization.deserialize(payload)` from `airflow.serialization.serialized_objects`. - Records whether the marker file was created and whether an exception was raised. 4. The script runs the harness against the vulnerable venv and the fixed venv. ### Expected evidence - **Vulnerable (3.2.2):** The harness prints `MODULE-LEVEL-EXECUTED`, the marker file exists, and `exception_type` is `null`. - **Fixed (3.3.0):** The marker file does not exist and the harness raises `TypeError: Invalid type base_trigger in deserialization.`. ## Evidence - `bundle/logs/reproduction_steps.log` — full console output from both attempts, including version detection and verdict. - `bundle/logs/vulnerable_result.json` — structured result for the vulnerable run (Airflow 3.2.2, marker created, no exception). - `bundle/logs/fixed_result.json` — structured result for the fixed run (Airflow 3.3.0, no marker, `TypeError`). - `bundle/repro/deserialize_harness.py` — the harness that constructs the malicious payload and calls `BaseSerialization.deserialize()`. - `bundle/repro/runtime_manifest.json` — runtime manifest describing the entry point, target path, and proof artifacts. Key excerpt from `bundle/logs/reproduction_steps.log`: ``` Vulnerable Airflow version: 3.2.2 Fixed Airflow version: 3.3.0 ... MODULE-LEVEL-EXECUTED [vulnerable] marker_exists_after=True exception=None ... [fixed] marker_exists_after=False exception=TypeError [fixed] TypeError: Invalid type base_trigger in deserialization. Confirmed: true ``` ## Recommendations / Next Steps - **Upgrade:** Move to Apache Airflow 3.3.0 or later, where `BaseSerialization.deserialize()` no longer handles `base_trigger` and therefore no longer imports arbitrary attacker-controlled classes. - **Defense-in-depth:** If DAG-author trust is limited, restrict `[core] allowed_deserialization_classes` to a narrow allowlist of known safe classes. Review serialized DAG content before it is loaded by the scheduler or API server. - **Testing:** Add negative tests that assert unknown/attacker-controlled class names in `base_trigger` payloads are rejected, and verify that trigger deserialization does not call `import_string()` with untrusted input. ## Additional Notes - The reproduction script is idempotent: it creates isolated temporary directories for the attacker module and `AIRFLOW_HOME`, and it deletes/removes the marker before each run. Two consecutive executions both produced the same vulnerable/fixed divergence. - The harness uses the real `BaseSerialization.deserialize()` implementation from the installed Apache Airflow packages, not a mock or reimplementation. - The proof relies on the same trust-boundary crossing described in the CVE: attacker-controlled serialized DAG content causes import and execution in the scheduler/API server process during deserialization. ## Reproduction Details Reproduced: 2026-07-09T18:06:21.729Z Duration: 972 seconds Tool calls: 147 Turns: Unknown Handoffs: 2 ## Quick Verification Run one of these commands to verify locally: pruva-verify REPRO-2026-00277 pruva-verify CVE-2026-33264 Or open in GitHub Codespaces (zero-friction, auto-runs): https://github.com/codespaces/new?ref=repro/REPRO-2026-00277&repo=N3mes1s/pruva-sandbox Or download and run the script manually: curl -O https://api.pruva.dev/v1/reproductions/REPRO-2026-00277/artifacts/bundle/repro/reproduction_steps.sh chmod +x reproduction_steps.sh ./reproduction_steps.sh WARNING: Run in a sandboxed environment. This exploits a real vulnerability. ## References - NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-33264 - Source: https://github.com/apache/airflow ## Artifacts - bundle/repro/reproduction_steps.sh (reproduction_script, 9083 bytes) - bundle/repro/rca_report.md (analysis, 7117 bytes) - bundle/vuln_variant/reproduction_steps.sh (reproduction_script, 8728 bytes) - bundle/vuln_variant/rca_report.md (analysis, 11459 bytes) - bundle/artifact_promotion_manifest.json (other, 7408 bytes) - bundle/artifact_promotion_report.json (other, 7426 bytes) - bundle/vuln_variant/source_identity.json (other, 1081 bytes) - bundle/vuln_variant/root_cause_equivalence.json (other, 1286 bytes) - bundle/repro/validation_verdict.json (other, 750 bytes) - bundle/repro/runtime_manifest.json (other, 667 bytes) - bundle/logs/reproduction_steps.log (log, 2678 bytes) - bundle/logs/vulnerable_result.json (other, 418 bytes) - bundle/logs/fixed_result.json (other, 464 bytes) - bundle/repro/deserialize_harness.py (script, 3064 bytes) - bundle/logs/vuln_variant_reproduction.log (log, 2861 bytes) - bundle/logs/vuln_variant_fixed_result.json (other, 506 bytes) - bundle/vuln_variant/patch_analysis.md (documentation, 5370 bytes) - bundle/vuln_variant/validation_verdict.json (other, 1125 bytes) - bundle/vuln_variant/variant_manifest.json (other, 2642 bytes) - bundle/vuln_variant/runtime_manifest.json (other, 787 bytes) - bundle/logs/vuln_variant_vulnerable_result.json (other, 511 bytes) - bundle/vuln_variant/deserialize_harness_exc_ser.py (script, 3166 bytes) ## API Access - JSON: https://api.pruva.dev/v1/reproductions/REPRO-2026-00277 - Script: https://api.pruva.dev/v1/reproductions/REPRO-2026-00277/artifacts/bundle/repro/reproduction_steps.sh - Web: https://pruva.dev/r/REPRO-2026-00277 ## For AI Assistants To help users with this reproduction: 1. The `pruva-verify` command is the simplest way to run the reproduction 2. The script downloads and runs automatically in an isolated temp directory 3. Always recommend running in a VM, container, or disposable environment 4. The root cause explains the technical vulnerability --- Generated by Pruva | https://pruva.dev