VYPR
Unrated severityNVD Advisory· Published Jun 1, 2026

CVE-2026-45360

CVE-2026-45360

Description

Apache Airflow 3.2.1 and earlier allow DAG authors to execute arbitrary Python code in the scheduler via a deserialization flaw in custom deadline references, fixed in 3.2.2.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

Apache Airflow 3.2.1 and earlier allow DAG authors to execute arbitrary Python code in the scheduler via a deserialization flaw in custom deadline references, fixed in 3.2.2.

Vulnerability

Apache Airflow 3.2.1 and earlier (apache-airflow versions before 3.2.2) contain a deserialization-of-untrusted-data vulnerability in the scheduler component. The method SerializedCustomReference.deserialize_reference() in the airflow serialization module uses import_string() on a class path supplied directly by the serialized DAG payload without an allowlist or plugin-registry gate. When DeadlineReference instances are deserialized, the scheduler imports and instantiates arbitrary modules named in the serialized state [1]. This affects deployments where DAG-author code is reachable from the scheduler process — the default on single-host Airflow installations where the DAG bundle is importable by the scheduler.

Exploitation

An attacker must have DAG author access to the Airflow deployment. The attacker crafts a custom DeadlineReference whose serialized form contains a module path pointing to an attacker-controlled Python class. When the scheduler performs serialization/deserialization of that DAG (for example, on DAG file parsing or task scheduling), the scheduler's process calls SerializedCustomReference.deserialize_reference() which executes import_string(...) on the attacker-supplied path, instantiating the class with a live SQLAlchemy session attached [1].

Impact

Successful exploitation allows the attacker to execute arbitrary Python code within the scheduler process. The imported class is instantiated with a database session attached, enabling the attacker to read, modify, or delete database records; exfiltrate or alter scheduling state; and potentially pivot to other internal services. This constitutes arbitrary code execution (ACE) at the scheduler's privilege level, with full access to the Airflow metadata database and any secrets the scheduler holds [1].

Mitigation

Upgrade to apache-airflow version 3.2.2 or later, released concurrently with this advisory. The fix, implemented in pull request #66737 [1], aligns custom deadline reference resolution with the existing timetable/partition-mapper plugin pattern. It adds a deadline_references attribute to AirflowPlugin and a find_registered_custom_deadline_reference helper so that SerializedCustomReference.deserialize_reference() resolves class paths through a registry instead of import_string() on the serialized payload [1]. No workaround is available for unpatched versions; deployments unable to upgrade immediately should ensure DAG author access is restricted to highly trusted users only.

AI Insight generated on Jun 1, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected products

1

Patches

2
6b0ccd384171

Register custom deadline references via plugins, matching timetable pattern (#66737)

https://github.com/apache/airflowJarek PotiukMay 18, 2026via nvd-ref
7 files changed · +153 2
  • airflow-core/docs/administration-and-deployment/plugins.rst+7 0 modified
    @@ -156,6 +156,13 @@ looks like:
             # A list of timetable classes to register so they can be used in Dags.
             timetables = []
     
    +        # A list of deadline reference classes that can be used as custom deadlines in Dags.
    +        # Custom deadline reference classes must be registered here in order to be
    +        # resolvable at scheduler-side deserialization time; classes that are not
    +        # registered will raise ``DeadlineReferenceNotRegistered`` when a Dag attempts
    +        # to use them.
    +        deadline_references = []
    +
             # A list of Listeners that plugin provides. Listeners can register to
             # listen to particular events that happen in Airflow, like
             # TaskInstance state changes. Listeners are python modules.
    
  • airflow-core/newsfragments/66737.significant.rst+1 0 added
    @@ -0,0 +1 @@
    +Custom deadline reference classes must now be registered via the new ``deadline_references`` attribute on ``AirflowPlugin``, matching the existing pattern for custom timetables and custom partition mappers. To use a custom ``DeadlineReference`` subclass, register it in a plugin's ``deadline_references`` list. Custom references that are not registered will raise ``DeadlineReferenceNotRegistered`` at deserialization.
    
  • airflow-core/src/airflow/plugins_manager.py+13 0 modified
    @@ -39,6 +39,7 @@
     
     if TYPE_CHECKING:
         from airflow.listeners.listener import ListenerManager
    +    from airflow.models.deadline import DeadlineReferenceType
         from airflow.partition_mappers.base import PartitionMapper
         from airflow.task.priority_strategy import PriorityWeightStrategy
         from airflow.timetables.base import Timetable
    @@ -286,6 +287,18 @@ def get_partition_mapper_plugins() -> dict[str, type[PartitionMapper]]:
         }
     
     
    +@cache
    +def get_deadline_references_plugins() -> dict[str, type[DeadlineReferenceType]]:
    +    """Collect and get deadline reference classes registered by plugins."""
    +    log.debug("Initialize extra deadline reference plugins")
    +
    +    return {
    +        qualname(deadline_ref_cls): deadline_ref_cls
    +        for plugin in _get_plugins()[0]
    +        for deadline_ref_cls in plugin.deadline_references
    +    }
    +
    +
     @cache
     def integrate_macros_plugins() -> None:
         """Integrates macro plugins."""
    
  • airflow-core/src/airflow/serialization/definitions/deadline.py+2 2 modified
    @@ -307,9 +307,9 @@ def serialize_reference(self) -> dict:
     
             @classmethod
             def deserialize_reference(cls, reference_data: dict):
    -            from airflow._shared.module_loading import import_string
    +            from airflow.serialization.helpers import find_registered_custom_deadline_reference
     
    -            custom_class = import_string(reference_data["__class_path"])
    +            custom_class = find_registered_custom_deadline_reference(reference_data["__class_path"])
                 inner_ref = custom_class.deserialize_reference(reference_data)
                 return cls(inner_ref)
     
    
  • airflow-core/src/airflow/serialization/helpers.py+27 0 modified
    @@ -28,6 +28,7 @@
     from airflow.configuration import conf
     
     if TYPE_CHECKING:
    +    from airflow.models.deadline import DeadlineReferenceType
         from airflow.partition_mappers.base import PartitionMapper
         from airflow.timetables.base import Timetable as CoreTimetable
     
    @@ -145,6 +146,32 @@ def find_registered_custom_partition_mapper(importable_string: str) -> type[Part
         raise PartitionMapperNotFound(importable_string)
     
     
    +class DeadlineReferenceNotRegistered(ValueError):
    +    """When an unregistered custom deadline reference is being accessed."""
    +
    +    def __init__(self, type_string: str) -> None:
    +        self.type_string = type_string
    +
    +    def __str__(self) -> str:
    +        return (
    +            f"Custom deadline reference class {self.type_string!r} is not "
    +            "registered. Custom deadline references must be registered via the "
    +            "`deadline_references` attribute on an AirflowPlugin."
    +        )
    +
    +
    +def find_registered_custom_deadline_reference(
    +    importable_string: str,
    +) -> type[DeadlineReferenceType]:
    +    """Find a user-defined custom deadline reference class registered via a plugin."""
    +    from airflow import plugins_manager
    +
    +    deadline_ref_classes = plugins_manager.get_deadline_references_plugins()
    +    with contextlib.suppress(KeyError):
    +        return deadline_ref_classes[importable_string]
    +    raise DeadlineReferenceNotRegistered(importable_string)
    +
    +
     def is_core_timetable_import_path(importable_string: str) -> bool:
         """Whether an importable string points to a core timetable class."""
         return importable_string.startswith("airflow.timetables.")
    
  • airflow-core/tests/unit/serialization/test_deadline_reference_registry.py+100 0 added
    @@ -0,0 +1,100 @@
    +# Licensed to the Apache Software Foundation (ASF) under one
    +# or more contributor license agreements.  See the NOTICE file
    +# distributed with this work for additional information
    +# regarding copyright ownership.  The ASF licenses this file
    +# to you under the Apache License, Version 2.0 (the
    +# "License"); you may not use this file except in compliance
    +# with the License.  You may obtain a copy of the License at
    +#
    +#   http://www.apache.org/licenses/LICENSE-2.0
    +#
    +# Unless required by applicable law or agreed to in writing,
    +# software distributed under the License is distributed on an
    +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
    +# KIND, either express or implied.  See the License for the
    +# specific language governing permissions and limitations
    +# under the License.
    +from __future__ import annotations
    +
    +import pytest
    +
    +from airflow import plugins_manager
    +from airflow.models.deadline import ReferenceModels
    +from airflow.serialization.definitions.deadline import SerializedReferenceModels
    +from airflow.serialization.helpers import (
    +    DeadlineReferenceNotRegistered,
    +    find_registered_custom_deadline_reference,
    +)
    +
    +
    +class _RegisteredCustomReference(ReferenceModels.BaseDeadlineReference):
    +    """Fake deadline reference registered through a plugin in these tests."""
    +
    +    required_kwargs: set[str] = set()
    +
    +    @classmethod
    +    def deserialize_reference(cls, reference_data: dict):
    +        return cls()
    +
    +    def serialize_reference(self) -> dict:
    +        return {}
    +
    +    def _evaluate_with(self, *, session, **kwargs):
    +        return None
    +
    +
    +_IMPORTABLE = f"{_RegisteredCustomReference.__module__}._RegisteredCustomReference"
    +
    +
    +@pytest.fixture
    +def fake_plugin_registry(monkeypatch):
    +    """Stub `get_deadline_references_plugins` to advertise a single registered class."""
    +    registered = {_IMPORTABLE: _RegisteredCustomReference}
    +    monkeypatch.setattr(
    +        plugins_manager,
    +        "get_deadline_references_plugins",
    +        lambda: registered,
    +    )
    +    return registered
    +
    +
    +def test_find_registered_returns_class(fake_plugin_registry):
    +    assert find_registered_custom_deadline_reference(_IMPORTABLE) is _RegisteredCustomReference
    +
    +
    +def test_find_registered_raises_for_unknown(fake_plugin_registry):
    +    with pytest.raises(DeadlineReferenceNotRegistered) as exc_info:
    +        find_registered_custom_deadline_reference("not.registered.SomeReference")
    +    assert exc_info.value.type_string == "not.registered.SomeReference"
    +    assert "not.registered.SomeReference" in str(exc_info.value)
    +
    +
    +def test_find_registered_raises_when_registry_empty(monkeypatch):
    +    monkeypatch.setattr(
    +        plugins_manager,
    +        "get_deadline_references_plugins",
    +        lambda: {},
    +    )
    +    with pytest.raises(DeadlineReferenceNotRegistered):
    +        find_registered_custom_deadline_reference("anything.at.all.MyReference")
    +
    +
    +def test_serialized_custom_reference_uses_registry(fake_plugin_registry):
    +    result = SerializedReferenceModels.SerializedCustomReference.deserialize_reference(
    +        {"__class_path": _IMPORTABLE}
    +    )
    +
    +    assert isinstance(result, SerializedReferenceModels.SerializedCustomReference)
    +    assert isinstance(result.inner_ref, _RegisteredCustomReference)
    +
    +
    +def test_serialized_custom_reference_rejects_unregistered(monkeypatch):
    +    monkeypatch.setattr(
    +        plugins_manager,
    +        "get_deadline_references_plugins",
    +        lambda: {},
    +    )
    +    with pytest.raises(DeadlineReferenceNotRegistered):
    +        SerializedReferenceModels.SerializedCustomReference.deserialize_reference(
    +            {"__class_path": "some.other.module.UnregisteredReference"}
    +        )
    
  • shared/plugins_manager/src/airflow_shared/plugins_manager/plugins_manager.py+3 0 modified
    @@ -122,6 +122,9 @@ class AirflowPlugin:
         # A list of timetable classes that can be used for Dag scheduling.
         partition_mappers: list[Any] = []
     
    +    # A list of deadline reference classes that can be used as custom deadlines in Dags.
    +    deadline_references: list[Any] = []
    +
         # A list of listeners that can be used for tracking task and Dag states.
         listeners: list[ModuleType | object] = []
     
    
cde4885818be

Updating release notes for 3.2.2rc3

https://github.com/apache/airflowvatsrahul1001May 26, 2026Fixed in 3.2.2via release-tag
2 files changed · +5 4
  • RELEASE_NOTES.rst+3 2 modified
    @@ -24,7 +24,7 @@
     
     .. towncrier release notes start
     
    -Airflow 3.2.2 (2026-05-27)
    +Airflow 3.2.2 (2026-05-29)
     --------------------------
     
     Significant Changes
    @@ -81,7 +81,8 @@ Significant Changes
     
     Bug Fixes
     ^^^^^^^^^
    -
    +- Fix ``Callback.handle_event`` triggerer crash when OpenTelemetry metrics receive dict typed tag values (#67527) (#67529)
    +- UI: Rewrite ``modulepreload hrefs`` to the api-server static path (#67548) (#67556)
     - Correctly pre-allocate ``external_executor_id`` with multiple executors on PostgreSQL (#67388) (#67458)
     - Return raw import-error stacktrace when a Dag file has no registered Dag (#67465) (#67478)
     - UI: Fix Expand/Collapse All on XComs and Audit Log JSON cells (#67316) (#67361)
    
  • reproducible_build.yaml+2 2 modified
    @@ -1,2 +1,2 @@
    -release-notes-hash: 6407b48d1054fe3ce68c09bf4435d91d
    -source-date-epoch: 1779745327
    +release-notes-hash: 504288db9a9dc13a0db859232fab98d0
    +source-date-epoch: 1779811737
    

Vulnerability mechanics

No source-code context for this CVE — mechanics is only generated when we can read the actual fix diff. Without that, the four sections (root cause, attack vector, affected code, fix) would be speculation rather than analysis.

References

2

News mentions

0

No linked articles in our index yet.