Apache Airflow Providers Http: Unsafe Pickle Deserialization in apache-airflow-providers-http leading to RCE via HttpOperator
Description
A user with access to the DB could craft a database entry that would result in executing code on Triggerer - which gives anyone who have access to DB the same permissions as Dag Author. Since direct DB access is not usual and recommended for Airflow, the likelihood of it making any damage is low.
You should upgrade to version 6.0.0 of the provider to avoid even that risk.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
A deserialization vulnerability in Apache Airflow's HTTP trigger allows users with DB access to execute code on the Triggerer, escalating privileges to Dag Author.
Vulnerability
Description CVE-2025-69219 is a deserialization vulnerability in Apache Airflow's HTTP trigger. The trigger previously used pickle to deserialize data from the database, allowing an attacker with direct database access to craft a malicious entry that executes arbitrary code on the Triggerer [3][4]. This bug was introduced because pickle deserialization can instantiate arbitrary objects and execute code if untrusted data is processed.
Exploitation
Conditions Exploitation requires direct database access, which is not the recommended method for managing Airflow [1][2]. An attacker with such access can modify database entries to include malicious serialized payloads. When the Triggerer processes these entries during deferred HTTP task execution, the pickle deserialization triggers code execution on the Triggerer node.
Impact
Successful exploitation grants the attacker the same permissions as a Dag Author [2]. This allows workflow manipulation, including creating or modifying DAGs, which could lead to further disruption within the Airflow environment. However, the requirement for direct DB access lowers the practical likelihood of exploitation in well-maintained deployments.
Mitigation
The vulnerability is fixed in provider version 6.0.0 by replacing pickle serialization with JSON serialization in the HTTP trigger [3][4]. Users are strongly recommended to upgrade to version 6.0.0 or later to mitigate this risk. No workarounds are provided due to the low likelihood of exploitation with proper database access controls.
- GitHub - apache/airflow: Apache Airflow - A platform to programmatically author, schedule, and monitor workflows
- NVD - CVE-2025-69219
- Replace pickle with json serialization for http triggers by amoghrajesh · Pull Request #61662 · apache/airflow
- Replace pickle with json serialization for http triggers (#61662) · apache/airflow@97839f7
AI Insight generated on May 18, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
apache-airflow-providers-httpPyPI | < 6.0.0 | 6.0.0 |
Affected products
1- Apache Software Foundation/Apache Airflow Providers Httpv5Range: 5.1.0
Patches
197839f7b0a8aReplace pickle with json serialization for http triggers (#61662)
9 files changed · +100 −25
providers/http/docs/changelog.rst+14 −0 modified@@ -27,6 +27,20 @@ Changelog --------- +6.0.0 +..... + +Breaking changes +~~~~~~~~~~~~~~~~ + +.. warning:: + The HTTP provider now uses JSON-based serialization for HTTP responses in deferred tasks + instead of pickle. Deferred HTTP tasks from previous provider versions will fail with a + ``TypeError`` after upgrade. + + Before upgrading, ensure all HTTP tasks with ``deferrable=True`` that are currently in + ``deferred`` state have completed or been cleared. + 5.6.4 .....
providers/http/docs/index.rst+3 −3 modified@@ -78,7 +78,7 @@ apache-airflow-providers-http package `Hypertext Transfer Protocol (HTTP) <https://www.w3.org/Protocols/>`__ -Release: 5.6.4 +Release: 6.0.0 Provider package ---------------- @@ -134,5 +134,5 @@ Downloading official packages You can download officially released packages and verify their checksums and signatures from the `Official Apache Download site <https://downloads.apache.org/airflow/providers/>`_ -* `The apache-airflow-providers-http 5.6.4 sdist package <https://downloads.apache.org/airflow/providers/apache_airflow_providers_http-5.6.4.tar.gz>`_ (`asc <https://downloads.apache.org/airflow/providers/apache_airflow_providers_http-5.6.4.tar.gz.asc>`__, `sha512 <https://downloads.apache.org/airflow/providers/apache_airflow_providers_http-5.6.4.tar.gz.sha512>`__) -* `The apache-airflow-providers-http 5.6.4 wheel package <https://downloads.apache.org/airflow/providers/apache_airflow_providers_http-5.6.4-py3-none-any.whl>`_ (`asc <https://downloads.apache.org/airflow/providers/apache_airflow_providers_http-5.6.4-py3-none-any.whl.asc>`__, `sha512 <https://downloads.apache.org/airflow/providers/apache_airflow_providers_http-5.6.4-py3-none-any.whl.sha512>`__) +* `The apache-airflow-providers-http 6.0.0 sdist package <https://downloads.apache.org/airflow/providers/apache_airflow_providers_http-6.0.0.tar.gz>`_ (`asc <https://downloads.apache.org/airflow/providers/apache_airflow_providers_http-6.0.0.tar.gz.asc>`__, `sha512 <https://downloads.apache.org/airflow/providers/apache_airflow_providers_http-6.0.0.tar.gz.sha512>`__) +* `The apache-airflow-providers-http 6.0.0 wheel package <https://downloads.apache.org/airflow/providers/apache_airflow_providers_http-6.0.0-py3-none-any.whl>`_ (`asc <https://downloads.apache.org/airflow/providers/apache_airflow_providers_http-6.0.0-py3-none-any.whl.asc>`__, `sha512 <https://downloads.apache.org/airflow/providers/apache_airflow_providers_http-6.0.0-py3-none-any.whl.sha512>`__)
providers/http/provider.yaml+1 −0 modified@@ -28,6 +28,7 @@ source-date-epoch: 1769537221 # In such case adding >= NEW_VERSION and bumping to NEW_VERSION in a provider have # to be done in the same PR versions: + - 6.0.0 - 5.6.4 - 5.6.3 - 5.6.2
providers/http/pyproject.toml+3 −3 modified@@ -25,7 +25,7 @@ build-backend = "flit_core.buildapi" [project] name = "apache-airflow-providers-http" -version = "5.6.4" +version = "6.0.0" description = "Provider package apache-airflow-providers-http for Apache Airflow" readme = "README.rst" license = "Apache-2.0" @@ -103,8 +103,8 @@ apache-airflow-providers-common-sql = {workspace = true} apache-airflow-providers-standard = {workspace = true} [project.urls] -"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-http/5.6.4" -"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-http/5.6.4/changelog.html" +"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-http/6.0.0" +"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-http/6.0.0/changelog.html" "Bug Tracker" = "https://github.com/apache/airflow/issues" "Source Code" = "https://github.com/apache/airflow" "Slack Chat" = "https://s.apache.org/airflow-slack"
providers/http/src/airflow/providers/http/__init__.py+1 −1 modified@@ -29,7 +29,7 @@ __all__ = ["__version__"] -__version__ = "5.6.4" +__version__ = "6.0.0" if packaging.version.parse(packaging.version.parse(airflow_version).base_version) < packaging.version.parse( "2.11.0"
providers/http/src/airflow/providers/http/operators/http.py+2 −4 modified@@ -17,16 +17,14 @@ # under the License. from __future__ import annotations -import base64 -import pickle from collections.abc import Callable, Sequence from typing import TYPE_CHECKING, Any from aiohttp import BasicAuth from requests import Response from airflow.providers.common.compat.sdk import AirflowException, BaseHook, BaseOperator, conf -from airflow.providers.http.triggers.http import HttpTrigger, serialize_auth_type +from airflow.providers.http.triggers.http import HttpResponseSerializer, HttpTrigger, serialize_auth_type from airflow.utils.helpers import merge_dicts if TYPE_CHECKING: @@ -286,7 +284,7 @@ def execute_complete( Relies on trigger to throw an exception, otherwise it assumes execution was successful. """ if event["status"] == "success": - response = pickle.loads(base64.standard_b64decode(event["response"])) + response = HttpResponseSerializer.deserialize(event["response"]) self.paginate_async(context=context, response=response, previous_responses=paginated_responses) return self.process_response(context=context, response=response)
providers/http/src/airflow/providers/http/triggers/http.py+47 −3 modified@@ -20,7 +20,6 @@ import base64 import importlib import inspect -import pickle import sys from collections.abc import AsyncIterator from importlib import import_module @@ -61,6 +60,51 @@ def deserialize_auth_type(path: str | None) -> type | None: return getattr(import_module(module_path), cls_name) +class HttpResponseSerializer: + """Serializer for requests.Response objects used in deferred HTTP tasks.""" + + @staticmethod + def serialize(response: requests.Response) -> dict[str, Any]: + """Convert a requests.Response object to a JSON serializable dictionary.""" + return { + "status_code": response.status_code, + "headers": dict(response.headers), + "content": base64.standard_b64encode(response.content).decode("ascii"), + "url": response.url, + "reason": response.reason, + "encoding": response.encoding, + "cookies": {k: v for k, v in response.cookies.items()}, + "history": [HttpResponseSerializer.serialize(h) for h in response.history], + } + + @staticmethod + def deserialize(data: dict[str, Any] | str) -> requests.Response: + """Reconstruct a requests.Response object from serialized data.""" + if isinstance(data, str): + raise TypeError("Response data must be a dict, got str") + + if not isinstance(data, dict): + raise TypeError(f"Expected dict, got {type(data).__name__}") + + response = requests.Response() + response.status_code = data["status_code"] + response.headers = CaseInsensitiveDict(data["headers"]) + response._content = base64.standard_b64decode(data["content"]) + response.url = data["url"] + response.reason = data.get("reason", "") + response.encoding = data.get("encoding") + + cookies = RequestsCookieJar() + for name, value in data.get("cookies", {}).items(): + cookies.set(name, str(value)) + response.cookies = cookies + + if data.get("history"): + response.history = [HttpResponseSerializer.deserialize(hist) for hist in data["history"]] + + return response + + class HttpTrigger(BaseTrigger): """ HttpTrigger run on the trigger worker. @@ -121,7 +165,7 @@ async def run(self) -> AsyncIterator[TriggerEvent]: yield TriggerEvent( { "status": "success", - "response": base64.standard_b64encode(pickle.dumps(response)).decode("ascii"), + "response": HttpResponseSerializer.serialize(response), } ) except Exception as e: @@ -301,7 +345,7 @@ async def run(self) -> AsyncIterator[TriggerEvent]: yield TriggerEvent( { "status": "success", - "response": base64.standard_b64encode(pickle.dumps(response)).decode("ascii"), + "response": HttpResponseSerializer.serialize(response), } ) except Exception as e:
providers/http/tests/unit/http/operators/test_http.py+21 −3 modified@@ -36,7 +36,7 @@ from airflow.providers.common.compat.sdk import AirflowException, TaskDeferred from airflow.providers.http.hooks.http import HttpHook from airflow.providers.http.operators.http import HttpOperator -from airflow.providers.http.triggers.http import HttpTrigger, serialize_auth_type +from airflow.providers.http.triggers.http import HttpResponseSerializer, HttpTrigger, serialize_auth_type @mock.patch.dict("os.environ", AIRFLOW_CONN_HTTP_EXAMPLE="http://www.example.com") @@ -124,11 +124,29 @@ def test_async_execute_successfully(self, requests_mock): context={}, event={ "status": "success", - "response": base64.standard_b64encode(pickle.dumps(response)).decode("ascii"), + "response": HttpResponseSerializer.serialize(response), }, ) assert result == "content" + def test_async_execute_legacy_pickle_format_raise_error(self): + """Test error raised with legacy pickle format.""" + operator = HttpOperator( + task_id="test_HTTP_op", + deferrable=True, + ) + response = Response() + response._content = b"content" + + with pytest.raises(TypeError, match="Response data must be a dict, got str"): + _ = operator.execute_complete( + context={}, + event={ + "status": "success", + "response": base64.standard_b64encode(pickle.dumps(response)).decode("ascii"), + }, + ) + @pytest.mark.parametrize( ( "data", @@ -223,7 +241,7 @@ def create_resume_response_parameters() -> dict: context={}, event={ "status": "success", - "response": base64.standard_b64encode(pickle.dumps(response)).decode("ascii"), + "response": HttpResponseSerializer.serialize(response), }, )
providers/http/tests/unit/http/triggers/test_http.py+8 −8 modified@@ -17,8 +17,6 @@ # under the License. from __future__ import annotations -import base64 -import pickle from asyncio import Future from http.cookies import SimpleCookie from typing import Any @@ -31,7 +29,12 @@ from yarl import URL from airflow.models import Connection -from airflow.providers.http.triggers.http import HttpEventTrigger, HttpSensorTrigger, HttpTrigger +from airflow.providers.http.triggers.http import ( + HttpEventTrigger, + HttpResponseSerializer, + HttpSensorTrigger, + HttpTrigger, +) from airflow.triggers.base import TriggerEvent HTTP_PATH = "airflow.providers.http.triggers.http.{}" @@ -144,10 +147,7 @@ async def test_trigger_on_success_yield_successfully(self, mock_hook, trigger, c generator = trigger.run() actual = await generator.asend(None) assert actual == TriggerEvent( - { - "status": "success", - "response": base64.standard_b64encode(pickle.dumps(response)).decode("ascii"), - } + {"status": "success", "response": HttpResponseSerializer.serialize(response)} ) @pytest.mark.asyncio @@ -252,7 +252,7 @@ async def test_trigger_on_success_yield_successfully(self, mock_hook, event_trig assert actual == TriggerEvent( { "status": "success", - "response": base64.standard_b64encode(pickle.dumps(response)).decode("ascii"), + "response": HttpResponseSerializer.serialize(response), } ) assert mock_hook.return_value.run.call_count == 2
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
6- github.com/apache/airflow/pull/61662ghsapatchWEB
- github.com/advisories/GHSA-9r5j-7r2x-rv4gghsaADVISORY
- lists.apache.org/thread/zjkfb2njklro68tqzym092r4w65m5dq0ghsavendor-advisoryWEB
- nvd.nist.gov/vuln/detail/CVE-2025-69219ghsaADVISORY
- www.openwall.com/lists/oss-security/2026/03/09/1ghsaWEB
- github.com/apache/airflow/commit/97839f7b0a8ae66d6079bb7fad5a363068f61617ghsaWEB
News mentions
0No linked articles in our index yet.