# 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.
