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