CVE-2026-42359
Description
Apache Airflow XCom PATCH bypasses reserved key check, allowing RCE on triggerer via serialized payload.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Apache Airflow XCom PATCH bypasses reserved key check, allowing RCE on triggerer via serialized payload.
Vulnerability
The XCom PATCH endpoint PATCH /api/v2/xcomEntries/{key} in Apache Airflow lacks the FORBIDDEN_XCOM_KEYS validation that was applied to the POST endpoint in CVE-2026-33858 fix. This allows authenticated users with XCom write permission on a DAG to set XCom entries under reserved key names (e.g., return_value) and accept serialized payload shapes that the triggerer's deserializer treats as code. The vulnerability affects Airflow deployments where untrusted users have XCom write permission on Dags that defer to the triggerer. Versions prior to 3.2.2 are affected [1].
Exploitation
An attacker needs to be an authenticated UI/API user with XCom write permission on a DAG that defers to the triggerer. The attacker sends a PATCH request to /api/v2/xcomEntries/{key} with a payload containing reserved keys (e.g., __classname__, __type, __var, __data__) and serialized code. The endpoint accepts the payload without validation, and when the task next defers, the triggerer deserializes the payload, leading to code execution [1].
Impact
Successful exploitation allows remote code execution (RCE) on the triggerer process with the privileges of the Airflow triggerer. This can lead to full compromise of the triggerer node, including data exfiltration, lateral movement, and further abuse of the Airflow environment.
Mitigation
Upgrade to apache-airflow version 3.2.2 or later, which includes the fix from pull request #65915 that adds the reserved-key check to the PATCH endpoint [1]. No viable workaround exists for affected versions; immediate upgrade is recommended.
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
1Patches
2c1734893709cApply reserved-key check to XCom update payload (#65915)
2 files changed · +48 −19
airflow-core/src/airflow/api_fastapi/core_api/datamodels/xcom.py+28 −19 modified@@ -83,6 +83,28 @@ class XComCollectionResponse(BaseModel): total_entries: int +def _check_forbidden_xcom_keys(value: Any) -> Any: + """Recursively reject forbidden deserialization keys in user-provided XCom data.""" + from airflow._shared.serialization import FORBIDDEN_XCOM_KEYS + + def _walk(obj: Any, path: str = "value") -> None: + if isinstance(obj, dict): + found = FORBIDDEN_XCOM_KEYS & obj.keys() + if found: + raise ValueError( + f"XCom {path} contains reserved serialization keys: {', '.join(sorted(found))}. " + f"These keys are reserved for internal use." + ) + for k, v in obj.items(): + _walk(v, f"{path}.{k}") + elif isinstance(obj, (list, tuple)): + for i, item in enumerate(obj): + _walk(item, f"{path}[{i}]") + + _walk(value) + return value + + class XComCreateBody(StrictBaseModel): """Payload serializer for creating an XCom entry.""" @@ -93,29 +115,16 @@ class XComCreateBody(StrictBaseModel): @field_validator("value") @classmethod def _check_forbidden_keys(cls, value: Any) -> Any: - """Recursively check for forbidden deserialization keys in user-provided XCom data.""" - from airflow._shared.serialization import FORBIDDEN_XCOM_KEYS - - def _walk_forbidden_keys(obj: Any, path: str = "value") -> None: - if isinstance(obj, dict): - found = FORBIDDEN_XCOM_KEYS & obj.keys() - if found: - raise ValueError( - f"XCom {path} contains reserved serialization keys: {', '.join(sorted(found))}. " - f"These keys are reserved for internal use." - ) - for k, v in obj.items(): - _walk_forbidden_keys(v, f"{path}.{k}") - elif isinstance(obj, (list, tuple)): - for i, item in enumerate(obj): - _walk_forbidden_keys(item, f"{path}[{i}]") - - _walk_forbidden_keys(value) - return value + return _check_forbidden_xcom_keys(value) class XComUpdateBody(StrictBaseModel): """Payload serializer for updating an XCom entry.""" value: Any map_index: int = -1 + + @field_validator("value") + @classmethod + def _check_forbidden_keys(cls, value: Any) -> Any: + return _check_forbidden_xcom_keys(value)
airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_xcom.py+20 −0 modified@@ -865,6 +865,26 @@ def test_patch_xcom_entry_with_slash_key(self, test_client, session): assert response.json()["value"] == new_value check_last_log(session, dag_id=TEST_DAG_ID, event="update_xcom_entry", logical_date=None) + @pytest.mark.parametrize( + ("key", "value"), + [ + ("__classname__", {"__classname__": "airflow.sdk.definitions.connection.Connection"}), + ("__type", {"__type": "airflow.sdk.definitions.connection.Connection", "__var": {}}), + ("__data__", {"nested": {"__data__": "malicious"}}), + ], + ) + def test_patch_xcom_entry_blocks_forbidden_keys(self, test_client, key, value): + """Test that XCom update blocks deserialization metadata keys.""" + self._create_xcom(TEST_XCOM_KEY, TEST_XCOM_VALUE) + response = test_client.patch( + f"/dags/{TEST_DAG_ID}/dagRuns/{run_id}/taskInstances/{TEST_TASK_ID}/xcomEntries/{TEST_XCOM_KEY}", + json={"value": value, "map_index": -1}, + ) + assert response.status_code == 422 + detail = str(response.json()["detail"]) + assert "reserved serialization keys" in detail + assert key in detail + def test_patch_xcom_preserves_int_type(self, test_client, session): """Test scenario described in #59032: if existing XCom value type is int, after patching with different value, it should still be int in the API response.
cde4885818beUpdating release notes for 3.2.2rc3
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
2News mentions
0No linked articles in our index yet.