diff --git a/airflow/config_templates/config.yml b/airflow/config_templates/config.yml
index 72672361a5cbe..ee246e1c0156e 100644
--- a/airflow/config_templates/config.yml
+++ b/airflow/config_templates/config.yml
@@ -1828,6 +1828,21 @@ webserver:
type: boolean
example: ~
default: "False"
+ allow_raw_html_descriptions:
+ description: |
+ A DAG author is able to provide any raw HTML into ``doc_md`` or params description in
+ ``description_md`` for text formatting. This is including potentially unsafe javascript.
+ Displaying the DAG or trigger form in web UI provides the DAG author the potential to
+ inject malicious code into clients browsers. To ensure the web UI is safe by default,
+ raw HTML is disabled by default. If you trust your DAG authors, you can enable HTML
+ support in markdown by setting this option to True.
+
+ This parameter also enables the deprecated fields ``description_html`` and
+ ``custom_html_form`` in DAG params until the feature is removed in a future version.
+ version_added: 2.8.0
+ type: boolean
+ example: "False"
+ default: "False"
email:
description: |
Configuration email backend and whether to
diff --git a/airflow/example_dags/example_params_ui_tutorial.py b/airflow/example_dags/example_params_ui_tutorial.py
index 12992c545c25a..489a4681c91b7 100644
--- a/airflow/example_dags/example_params_ui_tutorial.py
+++ b/airflow/example_dags/example_params_ui_tutorial.py
@@ -47,18 +47,17 @@
"flag": False,
"a_simple_list": ["one", "two", "three", "actually one value is made per line"],
# But of course you might want to have it nicer! Let's add some description to parameters.
- # Note if you can add any HTML formatting to the description, you need to use the description_html
+ # Note if you can add any Markdown formatting to the description, you need to use the description_md
# attribute.
"most_loved_number": Param(
42,
type="integer",
title="Your favorite number",
- description_html="""Everybody should have a favorite number. Not only math teachers.
- If you can not think of any at the moment please think of the 42 which is very famous because
- of the book
-
- The Hitchhiker's Guide to the Galaxy""",
+ description_md="Everybody should have a **favorite** number. Not only _math teachers_. "
+ "If you can not think of any at the moment please think of the 42 which is very famous because"
+ "of the book [The Hitchhiker's Guide to the Galaxy]"
+ "(https://en.wikipedia.org/wiki/Phrases_from_The_Hitchhiker%27s_Guide_to_the_Galaxy#"
+ "The_Answer_to_the_Ultimate_Question_of_Life,_the_Universe,_and_Everything_is_42).",
),
# If you want to have a selection list box then you can use the enum feature of JSON schema
"pick_one": Param(
@@ -177,8 +176,8 @@
"optional text, you can trigger also w/o text",
type=["null", "string"],
title="Optional text field",
- description_html="This field is optional. As field content is JSON schema validated you must "
- "allow the null type.",
+ description_md="This field is optional. As field content is JSON schema validated you must "
+ "allow the `null` type.",
),
# You can arrange the entry fields in sections so that you can have a better overview for the user
# Therefore you can add the "section" attribute.
@@ -188,10 +187,10 @@
"length-checked-field",
type="string",
title="Text field with length check",
- description_html="""This field is required. And you need to provide something between 10 and 30
- characters. See the
-
- JSON schema description (string) in for more details""",
+ description_md="""This field is required. And you need to provide something between 10 and 30
+ characters. See the JSON
+ [schema description (string)](https://json-schema.org/understanding-json-schema/reference/string.html)
+ for more details""",
minLength=10,
maxLength=20,
section="JSON Schema validation options",
@@ -200,9 +199,10 @@
100,
type="number",
title="Number field with value check",
- description_html="""This field is required. You need to provide any number between 64 and 128.
- See the
- JSON schema description (numbers) in for more details""",
+ description_md="""This field is required. You need to provide any number between 64 and 128.
+ See the JSON
+ [schema description (numbers)](https://json-schema.org/understanding-json-schema/reference/numeric.html)
+ for more details""",
minimum=64,
maximum=128,
section="JSON Schema validation options",
@@ -217,9 +217,9 @@
),
"array_of_objects": Param(
[{"name": "account_name", "country": "country_name"}],
- "Array with complex objects and validation rules. "
- "See JSON Schema validation options in specs.",
+ description_md="Array with complex objects and validation rules. "
+ "See [JSON Schema validation options in specs]"
+ "(https://json-schema.org/understanding-json-schema/reference/array.html#items).",
type="array",
title="JSON array field",
items={
@@ -233,69 +233,6 @@
# then you can use the JSON schema option of passing constant values. These parameters
# will not be displayed but passed to the DAG
"hidden_secret_field": Param("constant value", const="constant value"),
- # Finally besides the standard provided field generator you can have you own HTML form code
- # injected - but be careful, you can also mess-up the layout!
- "color_picker": Param(
- "#FF8800",
- type="string",
- title="Pick a color",
- description_html="""This is a special HTML widget as custom implementation in the DAG code.
- It is templated with the following parameter to render proper HTML form fields:
-
{name}: Name of the HTML input field that is expected.{value}:
- (Default) value that should be displayed when showing/loading the form.updateJSONconf() to update
- the form data to be posted as dag_run.conf.<input name='{name}' value='{value}' onchange='updateJSONconf()' />
- """,
- custom_html_form="""
- | - - | - - |
- |
| - - | - - | |
| - - | - - |
description_md. "
+ "Raw HTML is deprecated and must be enabled via "
+ "webserver.allow_raw_html_descriptions configuration parameter. Using plain text "
+ "as fallback for these fields. "
+ f"description_html which is deprecated. "
+ "Please migrate to description_md."
+ ),
+ "warning",
+ )
+ if allow_raw_html_descriptions and any("custom_html_form" in p.schema for p in dag.params.values()):
+ flash(
+ Markup(
+ "The form params use custom_html_form definition. "
+ "This is deprecated with Airflow 2.8.0 and will be removed in a future release."
+ ),
+ "warning",
+ )
+
ui_fields_defined = any("const" not in f["schema"] for f in form_fields.values())
show_trigger_form_if_no_params = conf.getboolean("webserver", "show_trigger_form_if_no_params")
diff --git a/docs/apache-airflow/core-concepts/params.rst b/docs/apache-airflow/core-concepts/params.rst
index b2b95252ec719..72eb058d4b74c 100644
--- a/docs/apache-airflow/core-concepts/params.rst
+++ b/docs/apache-airflow/core-concepts/params.rst
@@ -173,6 +173,8 @@ JSON Schema Validation
Use Params to Provide a Trigger UI Form
---------------------------------------
+.. versionadded:: 2.6.0
+
:class:`~airflow.models.dag.DAG` level params are used to render a user friendly trigger form.
This form is provided when a user clicks on the "Trigger DAG" button.
@@ -189,8 +191,8 @@ The following features are supported in the Trigger UI Form:
- The :class:`~airflow.models.param.Param` attribute ``title`` is used to render the form field label of the entry box.
If no ``title`` is defined the parameter name/key is used instead.
- The :class:`~airflow.models.param.Param` attribute ``description`` is rendered below an entry field as help text in gray color.
- If you want to provide HTML tags for special formatting or links you need to use the Param attribute
- ``description_html``, see tutorial DAG ``example_params_ui_tutorial`` for an example.
+ If you want to provide special formatting or links you need to use the Param attribute
+ ``description_md``. See tutorial DAG ``example_params_ui_tutorial`` for an example.
- The :class:`~airflow.models.param.Param` attribute ``type`` influences how a field is rendered. The following types are supported:
.. list-table::
@@ -313,7 +315,6 @@ The following features are supported in the Trigger UI Form:
The ``const`` value must match the default value to pass `JSON Schema validation raw HTML"
+ rendered = wrapped_markdown(HTML)
+ if allow_html:
+ assert HTML in rendered
+ else:
+ from markupsafe import escape
+
+ assert escape(HTML) in rendered
@pytest.mark.db_test
diff --git a/tests/www/views/test_views_trigger_dag.py b/tests/www/views/test_views_trigger_dag.py
index c48e053639fd7..65ad8734d5140 100644
--- a/tests/www/views/test_views_trigger_dag.py
+++ b/tests/www/views/test_views_trigger_dag.py
@@ -31,7 +31,7 @@
from airflow.utils.types import DagRunType
from tests.test_utils.api_connexion_utils import create_test_client
from tests.test_utils.config import conf_vars
-from tests.test_utils.www import check_content_in_response
+from tests.test_utils.www import check_content_in_response, check_content_not_in_response
pytestmark = pytest.mark.db_test
@@ -236,6 +236,55 @@ def test_trigger_dag_params_render(admin_client, dag_maker, session, app, monkey
)
+@pytest.mark.parametrize("allow_html", [False, True])
+def test_trigger_dag_html_allow(admin_client, dag_maker, session, app, monkeypatch, allow_html):
+ """
+ Test that HTML is escaped per default in description.
+ """
+ from markupsafe import escape
+
+ DAG_ID = "params_dag"
+ HTML_DESCRIPTION1 = "HTML raw code."
+ HTML_DESCRIPTION2 = "HTML in md text."
+ expect_escape = not allow_html
+ with conf_vars({("webserver", "allow_raw_html_descriptions"): str(allow_html)}):
+ param1 = Param(
+ 42,
+ description_html=HTML_DESCRIPTION1,
+ type="integer",
+ minimum=1,
+ maximum=100,
+ )
+ param2 = Param(
+ 42,
+ description_md=HTML_DESCRIPTION2,
+ type="integer",
+ minimum=1,
+ maximum=100,
+ )
+ with monkeypatch.context() as m:
+ with dag_maker(
+ dag_id=DAG_ID, serialized=True, session=session, params={"param1": param1, "param2": param2}
+ ):
+ EmptyOperator(task_id="task1")
+
+ m.setattr(app, "dag_bag", dag_maker.dagbag)
+ resp = admin_client.get(f"dags/{DAG_ID}/trigger")
+
+ if expect_escape:
+ check_content_in_response(escape(HTML_DESCRIPTION1), resp)
+ check_content_in_response(escape(HTML_DESCRIPTION2), resp)
+ check_content_in_response(
+ "At least one field in the trigger form uses a raw HTML form definition.", resp
+ )
+ else:
+ check_content_in_response(HTML_DESCRIPTION1, resp)
+ check_content_in_response(HTML_DESCRIPTION2, resp)
+ check_content_not_in_response(
+ "At least one field in the trigger form uses a raw HTML form definition.", resp
+ )
+
+
def test_trigger_endpoint_uses_existing_dagbag(admin_client):
"""
Test that Trigger Endpoint uses the DagBag already created in views.py