This is an automated email from the ASF dual-hosted git repository.

shahar1 pushed a commit to branch v3-4-test
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/v3-4-test by this push:
     new 8a42fefe7d7 [v3-4-test] Reject reserved loop keys when deleting an 
XCom (#76892) (#76966)
8a42fefe7d7 is described below

commit 8a42fefe7d7cd8a0bc4d987b4688daca5faf14fc
Author: Jason(Zhe-You) Liu <[email protected]>
AuthorDate: Fri Nov 13 13:08:52 2026 +0800

    [v3-4-test] Reject reserved loop keys when deleting an XCom (#76892) 
(#76966)
    
    set_xcom already refuses _airflow_loop_* keys, so task code could delete 
loop XComs it can never write.
    
    The external clear target key check is left out, as v3-4-test does not have 
that key.
    
    (cherry picked from commit 54970db82f16d119d000118d11dc5b819ac11fe4)
---
 .../src/airflow/api_fastapi/common/xcom.py         | 27 ++++++++++++++++++++++
 .../api_fastapi/core_api/routes/public/xcom.py     | 11 +++------
 .../api_fastapi/execution_api/routes/xcoms.py      | 12 ++++++----
 .../execution_api/versions/head/test_xcoms.py      | 14 +++++++++++
 4 files changed, 52 insertions(+), 12 deletions(-)

diff --git a/airflow-core/src/airflow/api_fastapi/common/xcom.py 
b/airflow-core/src/airflow/api_fastapi/common/xcom.py
new file mode 100644
index 00000000000..4e36c744714
--- /dev/null
+++ b/airflow-core/src/airflow/api_fastapi/common/xcom.py
@@ -0,0 +1,27 @@
+# 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
+
+from fastapi import HTTPException, status
+
+from airflow.models.dynamic_region import LOOP_XCOM_PREFIX
+
+
+def reject_reserved_xcom_key(key: str) -> None:
+    """Reject an XCom key that only Airflow writes, from what the Execution 
API validated."""
+    if key.startswith(LOOP_XCOM_PREFIX):
+        raise HTTPException(status.HTTP_400_BAD_REQUEST, "Reserved loop XCom 
key")
diff --git 
a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/xcom.py 
b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/xcom.py
index 5467e87750e..123c894d699 100644
--- a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/xcom.py
+++ b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/xcom.py
@@ -49,6 +49,7 @@ from airflow.api_fastapi.common.parameters import (
     teams_filter_factory,
 )
 from airflow.api_fastapi.common.router import AirflowRouter
+from airflow.api_fastapi.common.xcom import reject_reserved_xcom_key
 from airflow.api_fastapi.core_api.datamodels.xcom import (
     XComCollectionResponse,
     XComCreateBody,
@@ -314,12 +315,6 @@ def get_xcom_entries(
     return XComCollectionResponse(xcom_entries=entries, 
total_entries=total_entries)
 
 
-def _reject_reserved_loop_key(key: str) -> None:
-    # Loop gates write these keys through the Execution API, which validates 
their values.
-    if key.startswith(LOOP_XCOM_PREFIX):
-        raise HTTPException(status.HTTP_400_BAD_REQUEST, "Reserved loop XCom 
key")
-
-
 @xcom_router.post(
     "",
     status_code=status.HTTP_201_CREATED,
@@ -346,7 +341,7 @@ def create_xcom_entry(
     """Create an XCom entry."""
     from airflow.models.dagrun import DagRun
 
-    _reject_reserved_loop_key(request_body.key)
+    reject_reserved_xcom_key(request_body.key)
 
     dag_run = session.scalar(select(DagRun).where(DagRun.dag_id == dag_id, 
DagRun.run_id == dag_run_id))
     # Validate Dag ID
@@ -451,7 +446,7 @@ def update_xcom_entry(
     dag_bag: DagBagDep,
 ) -> XComResponseNative:
     """Update an existing XCom entry."""
-    _reject_reserved_loop_key(xcom_key)
+    reject_reserved_xcom_key(xcom_key)
     scope = resolve_task_scope(
         dag_id=dag_id,
         run_id=dag_run_id,
diff --git a/airflow-core/src/airflow/api_fastapi/execution_api/routes/xcoms.py 
b/airflow-core/src/airflow/api_fastapi/execution_api/routes/xcoms.py
index 2ce88dc8d3c..a8f326d84be 100644
--- a/airflow-core/src/airflow/api_fastapi/execution_api/routes/xcoms.py
+++ b/airflow-core/src/airflow/api_fastapi/execution_api/routes/xcoms.py
@@ -32,6 +32,7 @@ from sqlalchemy.sql.selectable import Select
 
 from airflow.api_fastapi.common.dagbag import DagBagDep
 from airflow.api_fastapi.common.db.common import SessionDep
+from airflow.api_fastapi.common.xcom import reject_reserved_xcom_key
 from airflow.api_fastapi.core_api.base import BaseModel
 from airflow.api_fastapi.core_api.openapi.exceptions import 
create_openapi_http_exception_doc
 from airflow.api_fastapi.execution_api.datamodels.token import TIToken
@@ -46,7 +47,6 @@ from airflow.api_fastapi.execution_api.versions.v2026_10_30 
import IdentifyArchi
 from airflow.exceptions import TaskNotFound
 from airflow.models.dagrun import DagRun
 from airflow.models.dynamic_region import (
-    LOOP_XCOM_PREFIX,
     SENTINEL_REGION_ID,
     AmbiguousProducerError,
     build_loop_sequence_order,
@@ -706,8 +706,7 @@ def set_xcom(
     """Set an Airflow XCom."""
     from airflow.configuration import conf
 
-    if key.startswith(LOOP_XCOM_PREFIX):
-        raise HTTPException(status.HTTP_400_BAD_REQUEST, "Reserved loop XCom 
key")
+    reject_reserved_xcom_key(key)
 
     # Validate that the provided key is not empty
     # XCom keys must be non-empty strings to ensure proper data retrieval and 
avoid ambiguity.
@@ -788,7 +787,10 @@ def set_xcom(
     responses={
         **create_openapi_http_exception_doc(
             [
-                (status.HTTP_400_BAD_REQUEST, "The target is in a loop the 
caller is not in"),
+                (
+                    status.HTTP_400_BAD_REQUEST,
+                    "The key is reserved, or the target is in a loop the 
caller is not in",
+                ),
                 (status.HTTP_409_CONFLICT, "Several live task instances match 
the producer"),
             ]
         ),
@@ -807,6 +809,8 @@ def delete_xcom(
     token=_TriggerAwareToken,
 ):
     """Delete a single XCom Value."""
+    reject_reserved_xcom_key(key)
+
     owner = _find_writer_id(
         token,
         dag_id=dag_id,
diff --git 
a/airflow-core/tests/unit/api_fastapi/execution_api/versions/head/test_xcoms.py 
b/airflow-core/tests/unit/api_fastapi/execution_api/versions/head/test_xcoms.py
index 5117d5f651d..a9bf99e5c05 100644
--- 
a/airflow-core/tests/unit/api_fastapi/execution_api/versions/head/test_xcoms.py
+++ 
b/airflow-core/tests/unit/api_fastapi/execution_api/versions/head/test_xcoms.py
@@ -1595,6 +1595,20 @@ class TestXComsDeleteEndpoint:
             assert verify.get(XComModelV1, (ti.dag_run.id, ti.task_id, 
ti.map_index, "xcom_1")) is None
             assert XComModelV2.get_for_attempt(ti1.id, "xcom_1", 
session=verify) is not None
 
+    def test_xcom_delete_rejects_reserved_key(self, client, 
create_task_instance, authenticate_as, session):
+        key = "_airflow_loop_decision_reason"
+        ti = create_task_instance()
+        ti.xcom_push(key=key, value='"reason"', session=session)
+        session.commit()
+        authenticate_as(ti)
+
+        response = 
client.delete(f"/execution/xcoms/{ti.dag_id}/{ti.run_id}/{ti.task_id}/{key}")
+
+        assert response.status_code == 400
+        assert response.json()["detail"] == "Reserved loop XCom key"
+        with create_session(scoped=False) as verify:
+            assert XComModelV2.get_for_attempt(ti.id, key, session=verify) is 
not None
+
     @pytest.mark.parametrize(
         ("target_task_id", "ti_map_index"),
         [

Reply via email to