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.
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).
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 — the most severe class — typically remotely exploitable with severe impact. Treat as an emergency.
Affected apache/airflow Versions
apache/airflow · github versions < 3.3.0 are affected.
How to Reproduce CVE-2026-33264
pruva-verify REPRO-2026-00277 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 Proof of Reproduction for CVE-2026-33264
- reached the target end-to-end
- full exploit chain demonstrated
- high confidence
- the upstream fix blocks the same trigger
serialized DAG content with a base_trigger class path pointing to an attacker-controlled module
- airflow.serialization.serialized_objects.BaseSerialization.deserialize()
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
Root Cause and Exploit Chain for CVE-2026-33264
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_triggerpayload 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 withTypeError: Invalid type base_trigger in deserialization.and did not perform the import or write the marker. - Parity:
fullfor the library-level claim. The harness directly invokesBaseSerialization.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_apisurface.
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
- Run
bundle/repro/reproduction_steps.sh. - 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)
- 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_triggerpayload with__classname__pointing toevil_trigger.EvilTrigger. - Calls
BaseSerialization.deserialize(payload)fromairflow.serialization.serialized_objects. - Records whether the marker file was created and whether an exception was raised.
- Creates a temporary attacker-controlled Python module (
- 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, andexception_typeisnull. - 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 callsBaseSerialization.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 handlesbase_triggerand therefore no longer imports arbitrary attacker-controlled classes. - Defense-in-depth: If DAG-author trust is limited, restrict
[core] allowed_deserialization_classesto 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_triggerpayloads are rejected, and verify that trigger deserialization does not callimport_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
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
BaseTriggerimport and theisinstance(var, BaseTrigger)serialization branch. - The
type_ == DAT.BASE_TRIGGERdeserialization branch that calledimport_string(tr_cls_name). - The
TaskDeferredentry from the exception serialization tuple, because it was only used to carry a trigger over the AIP-44 RPC. - The
BaseSerialization.deserialize()call onStartTriggerArgs.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()withtype_ == "base_trigger".Bypass surface:
BaseSerialization.deserialize()withtype_ == "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., fromSerializedDagModel/airflow/utils/sqlalchemy.pyand the fulldeserialize_dagpath).Specific code path:
airflow/serialization/serialized_objects.py, inBaseSerialization.deserialize()around thetype_ == DAT.AIRFLOW_EXC_SERbranch: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_sernode, specifically theexc_cls_namestring, plus theargsandkwargsthat 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_serbranch was also observed in the currentmainbranch at commit0d9dd822d3ac0cb44380836c455baecdf25e2eff.Risk level: Critical.
Consequences: A malicious DAG author can embed a crafted
airflow_exc_serpayload 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_serpayload 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 originalbase_triggerpayload was blocked on 3.3.0 withTypeError: Invalid type base_trigger in deserialization., confirming the parent fix is in place but theairflow_exc_serpath bypasses it. - Parity:
fullfor 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 submittedlibrary_apisurface.
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(tag3.3.0, commit1438ea3587031417cc85d74323235cf087a058fb). - 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_SERbranch.
Reproduction Steps
- Run
bundle/vuln_variant/reproduction_steps.sh. - 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).
- 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_serpayload withexc_cls_namepointing toevil_exc.EvilExceptionand properly encodedkwargs. - Calls
BaseSerialization.deserialize(payload)fromairflow.serialization.serialized_objects. - Records whether the marker file was created and whether an exception was raised.
- Creates a temporary attacker-controlled Python module (
- The script runs the harness against the vulnerable venv and the fixed venv.
- The script exits
0if the bypass is confirmed (both versions create the marker), or1otherwise.
Expected evidence
- Vulnerable (3.2.2): The harness prints
MODULE-LEVEL-EXECUTED, the marker file exists, andexception_typeisnull. - Fixed (3.3.0): The same happens — the marker file exists and
exception_typeisnull, proving the fix did not cover this branch. - Parent fix still present: The original
base_triggerpayload is rejected on 3.3.0 withTypeError: 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 callsBaseSerialization.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
- Close the exception branch: Apply the same gating logic to the
DAT.AIRFLOW_EXC_SERbranch as the maintainers proposed in PR #68511. Do not callimport_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). - Audit all
import_string()sinks in the legacy serialization layer: The fix should not be type-specific. Any branch inBaseSerialization.deserialize()(ordecoders.py) that callsimport_string()on serialized data must validate the import path against an allowlist or a registry. - Enforce
allowed_deserialization_classesconsistently: The legacyBaseSerializationpath 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. - Regression tests: Add negative tests that assert arbitrary/attacker-controlled class names in
airflow_exc_serpayloads are rejected, and verify that the rejection happens beforeimport_string()is called. - Upgrade path: Users relying on the 3.3.0 fix should not assume complete protection. They should apply a narrow
allowed_deserialization_classesallowlist 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
mainbranch of apache/airflow at0d9dd822d3ac0cb44380836c455baecdf25e2eff, 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.
Artifacts and Evidence for CVE-2026-33264
Scripts, logs, diffs, and output captured during the reproduction.
How to Fix CVE-2026-33264
Upgrade apache/airflow · github to 3.3.0 or later.
FAQ: CVE-2026-33264
How does the CVE-2026-33264 exploit work?
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?
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?
How can I reproduce CVE-2026-33264?
base_trigger class path in serialized DAG content and shows the scheduler/API server importing and executing the attacker-controlled module during deserialization.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.