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

potiuk pushed a commit to branch v2-11-test
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/v2-11-test by this push:
     new b75f131aa1a Fix Task Instances list view rendering raw HTML instead of 
links (#62533)
b75f131aa1a is described below

commit b75f131aa1a1a6ffcc113629826682bb0d7be79a
Author: Nick Benthem <[email protected]>
AuthorDate: Fri Feb 27 10:53:25 2026 -0500

    Fix Task Instances list view rendering raw HTML instead of links (#62533)
    
    * Fix Task Instances list view rendering raw HTML instead of links
    
    The connexion 2.15 migration (#51681) inadvertently removed the
    Markup() wrapper from task_instance_link(), dag_link(), and
    dag_run_link() in www/utils.py. Without Markup, Jinja2 auto-escapes
    the returned HTML strings, causing raw <a href="..."> tags to display
    as plain text instead of clickable links.
    
    Wrap the f-string returns back in Markup() while keeping the explicit
    escape() calls on user-provided values for XSS safety.
    
    * Add newsfragment for raw HTML links bugfix
---
 airflow/www/utils.py           |  8 +++++---
 newsfragments/62533.bugfix.rst |  1 +
 tests/www/test_utils.py        | 39 ++++++++++++++++++++++++++++-----------
 3 files changed, 34 insertions(+), 14 deletions(-)

diff --git a/airflow/www/utils.py b/airflow/www/utils.py
index bb8a887c26f..bccc597c8de 100644
--- a/airflow/www/utils.py
+++ b/airflow/www/utils.py
@@ -458,7 +458,8 @@ def task_instance_link(attr):
         execution_date=execution_date,
         tab="graph",
     )
-    return f"""
+    return Markup(
+        f"""
         <span style="white-space: nowrap;">
         <a href="{url}">{escape(task_id)}</a>
         <a href="{url_root}" title="Filter on this task">
@@ -467,6 +468,7 @@ def task_instance_link(attr):
         </a>
         </span>
         """
+    )
 
 
 def state_token(state):
@@ -537,7 +539,7 @@ def dag_link(attr):
     if not dag_id:
         return Markup("None")
     url = url_for("Airflow.grid", dag_id=dag_id, execution_date=execution_date)
-    return f'<a href="{url}">{escape(dag_id)}</a>'
+    return Markup(f'<a href="{url}">{escape(dag_id)}</a>')
 
 
 def dag_run_link(attr):
@@ -556,7 +558,7 @@ def dag_run_link(attr):
         dag_run_id=run_id,
         tab="graph",
     )
-    return f'<a href="{url}">{escape(run_id)}</a>'
+    return Markup(f'<a href="{url}">{escape(run_id)}</a>')
 
 
 def _get_run_ordering_expr(name: str) -> ColumnOperators:
diff --git a/newsfragments/62533.bugfix.rst b/newsfragments/62533.bugfix.rst
new file mode 100644
index 00000000000..5bddc275e32
--- /dev/null
+++ b/newsfragments/62533.bugfix.rst
@@ -0,0 +1 @@
+Fix Task Instances list view rendering raw HTML instead of clickable links for 
Dag Id, Task Id, and Run Id columns.
diff --git a/tests/www/test_utils.py b/tests/www/test_utils.py
index 7d501de1a2f..b947e268dfd 100644
--- a/tests/www/test_utils.py
+++ b/tests/www/test_utils.py
@@ -232,21 +232,26 @@ class TestUtils:
     @pytest.mark.skip_if_database_isolation_mode
     @pytest.mark.db_test
     def test_task_instance_link(self):
+        from markupsafe import Markup
+
         from airflow.www.app import cached_app
 
         with cached_app(testing=True).test_request_context():
-            html = str(
-                utils.task_instance_link(
-                    {"dag_id": "<a&1>", "task_id": "<b2>", "map_index": 1, 
"execution_date": datetime.now()}
-                )
+            result = utils.task_instance_link(
+                {"dag_id": "<a&1>", "task_id": "<b2>", "map_index": 1, 
"execution_date": datetime.now()}
             )
 
-            html_map_index_none = str(
-                utils.task_instance_link(
-                    {"dag_id": "<a&1>", "task_id": "<b2>", "map_index": -1, 
"execution_date": datetime.now()}
-                )
+            result_map_index_none = utils.task_instance_link(
+                {"dag_id": "<a&1>", "task_id": "<b2>", "map_index": -1, 
"execution_date": datetime.now()}
             )
 
+        html = str(result)
+        html_map_index_none = str(result_map_index_none)
+
+        # Return type must be Markup so Jinja2 renders HTML instead of 
escaping it
+        assert isinstance(result, Markup)
+        assert isinstance(result_map_index_none, Markup)
+
         assert "%3Ca&1%3E" in html
         assert "%3Cb2%3E" in html
         assert "map_index" in html
@@ -262,11 +267,17 @@ class TestUtils:
     @pytest.mark.skip_if_database_isolation_mode
     @pytest.mark.db_test
     def test_dag_link(self):
+        from markupsafe import Markup
+
         from airflow.www.app import cached_app
 
         with cached_app(testing=True).test_request_context():
-            html = str(utils.dag_link({"dag_id": "<a&1>", "execution_date": 
datetime.now()}))
+            result = utils.dag_link({"dag_id": "<a&1>", "execution_date": 
datetime.now()})
 
+        html = str(result)
+
+        # Return type must be Markup so Jinja2 renders HTML instead of 
escaping it
+        assert isinstance(result, Markup)
         assert "%3Ca&1%3E" in html
         assert "<a&1>" not in html
 
@@ -285,13 +296,19 @@ class TestUtils:
     @pytest.mark.skip_if_database_isolation_mode
     @pytest.mark.db_test
     def test_dag_run_link(self):
+        from markupsafe import Markup
+
         from airflow.www.app import cached_app
 
         with cached_app(testing=True).test_request_context():
-            html = str(
-                utils.dag_run_link({"dag_id": "<a&1>", "run_id": "<b2>", 
"execution_date": datetime.now()})
+            result = utils.dag_run_link(
+                {"dag_id": "<a&1>", "run_id": "<b2>", "execution_date": 
datetime.now()}
             )
 
+        html = str(result)
+
+        # Return type must be Markup so Jinja2 renders HTML instead of 
escaping it
+        assert isinstance(result, Markup)
         assert "%3Ca&1%3E" in html
         assert "%3Cb2%3E" in html
         assert "<a&1>" not in html

Reply via email to