Skip to content

CVE-2026-33264: Verified Repro With Script Download

CVE-2026-33264: Apache Airflow DAG author RCE via unrestricted import string in BaseSerialization.deserialize

CVE-2026-33264 is verified against apache/airflow · github. Affected versions: < 3.3.0. Fixed in 3.3.0. Vulnerability class: RCE. This critical reproduction includes runnable sandbox proof, artifacts, and a plain-text agent view under REPRO-2026-00277.

REPRO-2026-00277 apache/airflow · github RCE Variant found Jul 9, 2026 CVE entry ↗ .txt
Severity
CRITICAL
CVSS
9.8
Confidence
HIGH
Reproduced in
16m 12s
Tool calls
147
Spend
$1.60
01 · Overview

What Is CVE-2026-33264?

CVE-2026-33264 is a critical remote code execution vulnerability (CWE-502) in Apache Airflow. A DAG author can cause arbitrary code to execute on the trusted Scheduler or API Server process when Airflow deserializes serialized DAG content. Pruva reproduced it (reproduction REPRO-2026-00277).

02 · Severity & CVSS

CVE-2026-33264 Severity & CVSS Score

CVE-2026-33264 is rated critical severity, with a CVSS base score of 9.8 out of 10.

CRITICAL threat level
9.8 / 10 CVSS base
Weakness CWE-502 — Deserialization of Untrusted Data

Critical — the most severe class — typically remotely exploitable with severe impact. Treat as an emergency.

03 · Affected Versions

Affected apache/airflow Versions

apache/airflow · github versions < 3.3.0 are affected.

How to Reproduce CVE-2026-33264

$ pruva-verify REPRO-2026-00277
or curl -O https://pruva.dev/api/v1/reproductions/REPRO-2026-00277/artifacts/bundle/repro/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-33264

Remote code execution — reproduced
  • reached the target end-to-end
  • full exploit chain demonstrated
  • high confidence
  • the upstream fix blocks the same trigger
Trigger

serialized DAG content with a base_trigger class path pointing to an attacker-controlled module

Attack chain
  1. airflow.serialization.serialized_objects.BaseSerialization.deserialize()
Variants tested

Bypass of CVE-2026-33264 fix via unprotected AIRFLOW_EXC_SER branch in BaseSerialization.deserialize(); Apache Airflow 3.3.0 still imports attacker-controlled module names from serialized DAG content.

How the agent worked 350 events · 147 tool calls · 16 min
16 minDuration
147Tool calls
98Reasoning steps
350Events
1Dead-ends
Agent activity over 16 min
Support
21
Hypothesis
2
Repro
59
Judge
22
Variant
242
0:0016:12

Root Cause and Exploit Chain for CVE-2026-33264

Versions: All versions before 3.3.0 (reproduced against 3.2.2)Fixed: 3.3.0 and later

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().

  • 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:

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.

Variant Analysis & Alternative Triggers for CVE-2026-33264

Versions: Reproduced against Apache Airflow 3.2.2 (vulnerable) and 3.3.0 (claimed fixed, but bypassed). The same airflow_exc_ser branch was also observed in the current main branch at commit 0d9dd822d3ac0cb44380836c455baecdf25e2eff.

Apache Airflow 3.3.0, which is the patched release for CVE-2026-33264, remains vulnerable to remote code execution when loading serialized DAG content. The 3.3.0 fix removed the DAT.BASE_TRIGGER branch in BaseSerialization.deserialize() but left the neighboring DAT.AIRFLOW_EXC_SER branch intact. That branch still calls import_string() on an attacker-controlled class name read from the serialized payload, so a DAG author can embed a malicious airflow_exc_ser node instead of a base_trigger node and achieve the same RCE on the Scheduler/API Server process. This is a true bypass of the 3.3.0 fix for the same root cause and trust boundary.

Fix Coverage / Assumptions

The original fix (Apache Airflow 3.3.0, PR #68528) assumes that the only dangerous import_string() sink in BaseSerialization.deserialize() was the DAT.BASE_TRIGGER branch. It removes:

  • The BaseTrigger import and the isinstance(var, BaseTrigger) serialization branch.
  • The type_ == DAT.BASE_TRIGGER deserialization branch that called import_string(tr_cls_name).
  • The TaskDeferred entry from the exception serialization tuple, because it was only used to carry a trigger over the AIP-44 RPC.
  • The BaseSerialization.deserialize() call on StartTriggerArgs.trigger_kwargs / next_kwargs (PR #66002), keeping them as raw JSON.

The fix does not modify the DAT.AIRFLOW_EXC_SER / DAT.BASE_EXC_SER branch in the same deserialize() method, nor does it apply the allowed_deserialization_classes allowlist to the legacy BaseSerialization path. Consequently, the same arbitrary import_string() sink remains reachable through a different serialized type.

Variant / Alternate Trigger

  • Parent surface: BaseSerialization.deserialize() with type_ == "base_trigger".

  • Bypass surface: BaseSerialization.deserialize() with type_ == "airflow_exc_ser".

  • Entry point: direct call to airflow.serialization.serialized_objects.BaseSerialization.deserialize(), which is the same recursive helper used when the Scheduler and API Server load serialized DAGs (e.g., from SerializedDagModel / airflow/utils/sqlalchemy.py and the full deserialize_dag path).

  • Specific code path: airflow/serialization/serialized_objects.py, in BaseSerialization.deserialize() around the type_ == DAT.AIRFLOW_EXC_SER branch:

    elif type_ == DAT.AIRFLOW_EXC_SER or type_ == DAT.BASE_EXC_SER:
        deser = cls.deserialize(var)
        exc_cls_name = deser["exc_cls_name"]
        args = deser["args"]
        kwargs = deser["kwargs"]
        del deser
        if type_ == DAT.AIRFLOW_EXC_SER:
            exc_cls = import_string(exc_cls_name)   # attacker-controlled import
        else:
            exc_cls = import_string(f"builtins.{exc_cls_name}")
        return exc_cls(*args, **kwargs)
    
  • Attacker-controlled input: the serialized airflow_exc_ser node, specifically the exc_cls_name string, plus the args and kwargs that are passed to the imported class constructor.

  • Package/component affected: Apache Airflow (airflow.serialization.serialized_objects.BaseSerialization.deserialize()).

  • Affected versions: Reproduced against Apache Airflow 3.2.2 (vulnerable) and 3.3.0 (claimed fixed, but bypassed). The same airflow_exc_ser branch was also observed in the current main branch at commit 0d9dd822d3ac0cb44380836c455baecdf25e2eff.

  • Risk level: Critical.

  • Consequences: A malicious DAG author can embed a crafted airflow_exc_ser payload in serialized DAG content. When the Scheduler or API Server deserializes it, the attacker-controlled module is imported and its module-level code executes in that process. The attacker also controls the constructor arguments of the imported class, so the instance creation itself can execute attacker-controlled __init__ code. This is the same trust-boundary crossing as the original CVE: DAG-author code executes in the Scheduler/API Server.

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 variant run: Code execution demonstrated. A crafted airflow_exc_ser payload caused both Apache Airflow 3.2.2 and the fixed 3.3.0 to import an attacker-controlled module and execute its module-level code, which wrote a marker file (PWNED). The original base_trigger payload was blocked on 3.3.0 with TypeError: Invalid type base_trigger in deserialization., confirming the parent fix is in place but the airflow_exc_ser path bypasses it.
  • Parity: full for the library-level claim. The same deserialization sink and the same trust-boundary crossing are reached; only the encoded type tag differs.
  • Not demonstrated: No live Scheduler or API Server process was started. The proof uses the same realistic BaseSerialization.deserialize() harness as the parent reproduction. A full end-to-end DAG file → database → Scheduler load would further strengthen the claim but is not required for the submitted library_api surface.

Root Cause

The root cause is identical to the parent CVE: BaseSerialization.deserialize() calls import_string() on a value read directly from the serialized payload without any allowlist or type validation. The 3.3.0 fix removed one instance of this pattern (DAT.BASE_TRIGGER) but left another instance in the same function (DAT.AIRFLOW_EXC_SER). The exc_cls_name field is attacker-controlled, so importing it loads the attacker module and executes module-level code. The instance is then constructed with attacker-controlled args/kwargs, so __init__ code also runs under attacker control.

  • Parent fix commit: f5f7953d949f61552c4feb8b3200be2a677a08c5 (tag 3.3.0, commit 1438ea3587031417cc85d74323235cf087a058fb).
  • Complementary fix mentioned by maintainers but not included: PR #68511 "Restrict exception-node deserialization to known classes without importing the stored name".
  • Bypassed code: airflow/serialization/serialized_objects.py, BaseSerialization.deserialize(), DAT.AIRFLOW_EXC_SER branch.

Reproduction Steps

  1. Run bundle/vuln_variant/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 parent version).
    • airflow_3.3.0_cve_2026_33264_venv — Apache Airflow 3.3.0 (claimed fixed).
  3. The script writes bundle/vuln_variant/deserialize_harness_exc_ser.py, which:
    • Creates a temporary attacker-controlled Python module (evil_exc.py) whose top-level code writes a marker file (PWNED) when imported.
    • Builds a serialized airflow_exc_ser payload with exc_cls_name pointing to evil_exc.EvilException and properly encoded kwargs.
    • 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.
  5. The script exits 0 if the bypass is confirmed (both versions create the marker), or 1 otherwise.
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 same happens — the marker file exists and exception_type is null, proving the fix did not cover this branch.
  • Parent fix still present: The original base_trigger payload is rejected on 3.3.0 with TypeError: Invalid type base_trigger in deserialization. (not directly tested by this variant script, but confirmed in the parent reproduction).

Evidence

  • bundle/logs/vuln_variant_reproduction.log — full console output from both attempts, including version detection and verdict.
  • bundle/logs/vuln_variant_vulnerable_result.json — structured result for the vulnerable run (Airflow 3.2.2, marker created, no exception).
  • bundle/logs/vuln_variant_fixed_result.json — structured result for the fixed run (Airflow 3.3.0, marker created, no exception).
  • bundle/vuln_variant/deserialize_harness_exc_ser.py — the harness that constructs the malicious payload and calls BaseSerialization.deserialize().
  • bundle/vuln_variant/runtime_manifest.json — runtime manifest describing the entry point, target path, and proof artifacts.
  • bundle/vuln_variant/patch_analysis.md — detailed comparison of the 3.2.2 vs 3.3.0 serialization code and the missed branch.

Key excerpt from bundle/logs/vuln_variant_reproduction.log:

Vulnerable Airflow version: 3.2.2
Fixed Airflow version: 3.3.0
...
MODULE-LEVEL-EXECUTED
[vulnerable] marker_exists_after=True exception=None
...
MODULE-LEVEL-EXECUTED
[fixed] marker_exists_after=True exception=None
...
Bypass confirmed: true
Bypass confirmed: fixed version remains vulnerable via airflow_exc_ser.

Recommendations / Next Steps

  1. Close the exception branch: Apply the same gating logic to the DAT.AIRFLOW_EXC_SER branch as the maintainers proposed in PR #68511. Do not call import_string(exc_cls_name) directly on a serialized value. Either validate the class against an allowlist before importing, or avoid importing from the stored name entirely (e.g., resolve only registered exception types by a safe mapping).
  2. Audit all import_string() sinks in the legacy serialization layer: The fix should not be type-specific. Any branch in BaseSerialization.deserialize() (or decoders.py) that calls import_string() on serialized data must validate the import path against an allowlist or a registry.
  3. Enforce allowed_deserialization_classes consistently: The legacy BaseSerialization path currently does not honor [core] allowed_deserialization_classes. Either make the legacy path respect the same allowlist or migrate the exception branch to the new SDK serde, which already uses it.
  4. Regression tests: Add negative tests that assert arbitrary/attacker-controlled class names in airflow_exc_ser payloads are rejected, and verify that the rejection happens before import_string() is called.
  5. Upgrade path: Users relying on the 3.3.0 fix should not assume complete protection. They should apply a narrow allowed_deserialization_classes allowlist if available, or wait for a follow-up release that fixes the exception branch.

Additional Notes

  • The reproduction script is idempotent: it creates isolated temporary directories for the attacker module and AIRFLOW_HOME, and it removes the marker before each run. Two consecutive executions both produced the same vulnerable/fixed behavior (both created the marker).
  • 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 as the parent CVE: attacker-controlled serialized DAG content causes import and execution in the scheduler/API server process during deserialization.
  • The bypass was also observed in the current main branch of apache/airflow at 0d9dd822d3ac0cb44380836c455baecdf25e2eff, indicating the issue is not resolved in the default development branch at the time of this run.

CVE-2026-33264 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:003:42
0:00
session startedaccounts/fireworks/models/kimi-k2p7-code · CVE-2026-33264 · REPRO-20
0:03
0:05
web search
0:07
0:08
0:13
0:15
web search
0:20
0:24
0:34
0:35
web search
0:54
0:54
extract_facts
no facts extracted
0:55
0:55
0:55
supportrepro
3:26
3:29
3:29
3:29
3:29
3:34
3:34
3:34
3:37
3:37
3:42
3:42
08 · How to Fix

How to Fix CVE-2026-33264

Upgrade apache/airflow · github to 3.3.0 or later.

Coming soon

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

10 · FAQ

FAQ: CVE-2026-33264

How does the CVE-2026-33264 exploit work?

A DAG author embeds a class path (for example inside a base_trigger payload) that resolves to a module they control into serialized DAG content. When the scheduler or API server later deserializes that DAG, BaseSerialization.deserialize() imports the module, executing its top-level code and crossing the trust boundary between untrusted DAG-author code and the trusted scheduler/API server process.

Which Airflow versions are affected by CVE-2026-33264, and where is it fixed?

All Apache Airflow versions before 3.3.0 are affected; it is fixed in 3.3.0, which removes deserialization of base_trigger payloads through BaseSerialization.deserialize(). As defense-in-depth, users can also restrict [core] allowed_deserialization_classes to a narrow allowlist.

How severe is CVE-2026-33264?

It is rated critical severity: a DAG author, who is normally a lower-trust role than the scheduler/API server, can achieve remote code execution in that trusted process.

How can I reproduce CVE-2026-33264?

Download the verified script from this page and run it in an isolated environment against Airflow 3.2.2 (a version before the 3.3.0 fix). It crafts a malicious base_trigger class path in serialized DAG content and shows the scheduler/API server importing and executing the attacker-controlled module during deserialization.
11 · References

References for CVE-2026-33264

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