CVE-2020-13944
Description
In Apache Airflow < 1.10.12, the "origin" parameter passed to some of the endpoints like '/trigger' was vulnerable to XSS exploit.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Apache Airflow before 1.10.12 contains a reflected XSS vulnerability in the 'origin' parameter of endpoints like '/trigger', allowing attackers to inject arbitrary web scripts.
Vulnerability
Overview
CVE-2020-13944 is a reflected cross-site scripting (XSS) vulnerability in Apache Airflow versions prior to 1.10.12. The origin parameter, passed to endpoints such as /trigger, was not properly sanitized before being reflected in the response. This allowed an attacker to inject arbitrary HTML or JavaScript code. The fix, introduced in commit [4], adds a get_safe_url function that validates the URL's scheme and netloc against the server's own host, preventing the injection of external or malicious URLs.
Exploitation
An attacker can exploit this vulnerability by crafting a malicious link containing a specially crafted origin parameter. The attack requires no authentication and can be delivered via social engineering (e.g., phishing email). When a victim clicks the link, the injected script executes in the context of the Airflow web UI session. The attack vector is network-based with low complexity, as noted in the GitHub Advisory [3].
Impact
Successful exploitation allows the attacker to execute arbitrary JavaScript in the victim's browser. This can lead to session hijacking, theft of sensitive data (e.g., cookies, authentication tokens), defacement of the web interface, or unauthorized actions performed on behalf of the victim. The vulnerability is rated as medium severity due to the need for user interaction.
Mitigation
Users should upgrade to Apache Airflow 1.10.12 or later, which includes the fix from commit [4]. No workarounds are available for earlier versions. The Apache Software Foundation has released advisories [2] and the fix is included in the official release. Organizations using Airflow should prioritize this update to prevent potential XSS attacks.
AI Insight generated on May 21, 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-airflowPyPI | < 1.10.12 | 1.10.12 |
Affected products
3- Apache/Airflowdescription
- osv-coords2 versions
< 1.10.15+ 1 more
- (no CPE)range: < 1.10.15
- (no CPE)range: < 1.10.12
Patches
15c2bb7b0b0e7Webserver: Sanitize values passed to origin param (#10334)
2 files changed · +38 −10
airflow/www/views.py+23 −10 modified@@ -27,7 +27,7 @@ from datetime import datetime, timedelta from json import JSONDecodeError from typing import Dict, List, Optional, Tuple -from urllib.parse import unquote +from urllib.parse import unquote, urlparse import lazy_object_proxy import nvd3 @@ -81,6 +81,19 @@ FILTER_STATUS_COOKIE = 'dag_status_filter' +def get_safe_url(url): + """Given a user-supplied URL, ensure it points to our web server""" + valid_schemes = ['http', 'https', ''] + valid_netlocs = [request.host, ''] + + parsed = urlparse(url) + + if parsed.scheme in valid_schemes and parsed.netloc in valid_netlocs: + return url + + return url_for('Airflow.index') + + def get_date_time_num_runs_dag_runs_form_data(request, session, dag): """Get Execution Data, Base Date & Number of runs from a Request """ dttm = request.args.get('execution_date') @@ -921,7 +934,7 @@ def xcom(self, session=None): def run(self): dag_id = request.form.get('dag_id') task_id = request.form.get('task_id') - origin = request.form.get('origin') + origin = get_safe_url(request.form.get('origin')) dag = current_app.dag_bag.get_dag(dag_id) task = dag.get_task(task_id) @@ -990,7 +1003,7 @@ def delete(self): from airflow.exceptions import DagFileExists, DagNotFound dag_id = request.values.get('dag_id') - origin = request.values.get('origin') or url_for('Airflow.index') + origin = get_safe_url(request.values.get('origin')) try: delete_dag.delete_dag(dag_id) @@ -1017,7 +1030,7 @@ def delete(self): def trigger(self, session=None): dag_id = request.values.get('dag_id') - origin = request.values.get('origin') or url_for('Airflow.index') + origin = get_safe_url(request.values.get('origin')) if request.method == 'GET': return self.render_template( @@ -1115,7 +1128,7 @@ def _clear_dag_tis(self, dag, start_date, end_date, origin, def clear(self): dag_id = request.form.get('dag_id') task_id = request.form.get('task_id') - origin = request.form.get('origin') + origin = get_safe_url(request.form.get('origin')) dag = current_app.dag_bag.get_dag(dag_id) execution_date = request.form.get('execution_date') @@ -1145,7 +1158,7 @@ def clear(self): @action_logging def dagrun_clear(self): dag_id = request.form.get('dag_id') - origin = request.form.get('origin') + origin = get_safe_url(request.form.get('origin')) execution_date = request.form.get('execution_date') confirmed = request.form.get('confirmed') == "true" @@ -1267,7 +1280,7 @@ def dagrun_failed(self): dag_id = request.form.get('dag_id') execution_date = request.form.get('execution_date') confirmed = request.form.get('confirmed') == 'true' - origin = request.form.get('origin') + origin = get_safe_url(request.form.get('origin')) return self._mark_dagrun_state_as_failed(dag_id, execution_date, confirmed, origin) @@ -1279,7 +1292,7 @@ def dagrun_success(self): dag_id = request.form.get('dag_id') execution_date = request.form.get('execution_date') confirmed = request.form.get('confirmed') == 'true' - origin = request.form.get('origin') + origin = get_safe_url(request.form.get('origin')) return self._mark_dagrun_state_as_success(dag_id, execution_date, confirmed, origin) @@ -1329,7 +1342,7 @@ def _mark_task_instance_state(self, dag_id, task_id, origin, execution_date, def failed(self): dag_id = request.form.get('dag_id') task_id = request.form.get('task_id') - origin = request.form.get('origin') + origin = get_safe_url(request.form.get('origin')) execution_date = request.form.get('execution_date') confirmed = request.form.get('confirmed') == "true" @@ -1349,7 +1362,7 @@ def failed(self): def success(self): dag_id = request.form.get('dag_id') task_id = request.form.get('task_id') - origin = request.form.get('origin') + origin = get_safe_url(request.form.get('origin')) execution_date = request.form.get('execution_date') confirmed = request.form.get('confirmed') == "true"
tests/www/test_views.py+15 −0 modified@@ -2468,6 +2468,21 @@ def test_trigger_dag_form(self): self.assertEqual(resp.status_code, 200) self.check_content_in_response('Trigger DAG: {}'.format(test_dag_id), resp) + @parameterized.expand([ + ("javascript:alert(1)", "/home"), + ("http://google.com", "/home"), + ("%2Ftree%3Fdag_id%3Dexample_bash_operator", "/tree?dag_id=example_bash_operator"), + ("%2Fgraph%3Fdag_id%3Dexample_bash_operator", "/graph?dag_id=example_bash_operator"), + ]) + def test_trigger_dag_form_origin_url(self, test_origin, expected_origin): + test_dag_id = "example_bash_operator" + + resp = self.client.get('trigger?dag_id={}&origin={}'.format(test_dag_id, test_origin)) + self.check_content_in_response( + '<button class="btn" onclick="location.href = \'{}\'; return false">'.format( + expected_origin), + resp) + def test_trigger_endpoint_uses_existing_dagbag(self): """ Test that Trigger Endpoint uses the DagBag already created in views.py
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
19- github.com/advisories/GHSA-4pwq-fj89-6rjcghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2020-13944ghsaADVISORY
- www.openwall.com/lists/oss-security/2020/12/11/2ghsamailing-listx_refsource_MLISTWEB
- www.openwall.com/lists/oss-security/2021/05/01/2ghsamailing-listx_refsource_MLISTWEB
- github.com/apache/airflow/commit/5c2bb7b0b0e717b11f093910b443243330ad93caghsaWEB
- github.com/pypa/advisory-database/tree/main/vulns/apache-airflow/PYSEC-2020-19.yamlghsaWEB
- lists.apache.org/thread.html/r2892ef594dbbf54d0939b808626f52f7c2d1584f8aa1d81570847d2a%40%3Cannounce.apache.org%3Emitremailing-listx_refsource_MLIST
- lists.apache.org/thread.html/r2892ef594dbbf54d0939b808626f52f7c2d1584f8aa1d81570847d2a%40%3Cdev.airflow.apache.org%3Emitremailing-listx_refsource_MLIST
- lists.apache.org/thread.html/r2892ef594dbbf54d0939b808626f52f7c2d1584f8aa1d81570847d2a%40%3Cusers.airflow.apache.org%3Emitremailing-listx_refsource_MLIST
- lists.apache.org/thread.html/r2892ef594dbbf54d0939b808626f52f7c2d1584f8aa1d81570847d2a@%3Cannounce.apache.org%3EghsaWEB
- lists.apache.org/thread.html/r2892ef594dbbf54d0939b808626f52f7c2d1584f8aa1d81570847d2a@%3Cdev.airflow.apache.org%3EghsaWEB
- lists.apache.org/thread.html/r2892ef594dbbf54d0939b808626f52f7c2d1584f8aa1d81570847d2a@%3Cusers.airflow.apache.org%3EghsaWEB
- lists.apache.org/thread.html/r4656959c8ed06c1f6202d89aa4e67b35ad7bdba5a666caff3fea888e%40%3Cusers.airflow.apache.org%3Emitremailing-listx_refsource_MLIST
- lists.apache.org/thread.html/r4656959c8ed06c1f6202d89aa4e67b35ad7bdba5a666caff3fea888e@%3Cusers.airflow.apache.org%3EghsaWEB
- lists.apache.org/thread.html/r97e1b60ca508a86be58c43f405c0c8ff00ba467ba0bee68704ae7e3e%40%3Cdev.airflow.apache.org%3Eghsax_refsource_MISCWEB
- lists.apache.org/thread.html/ra8ce70088ba291f358e077cafdb14d174b7a1ce9a9d86d1b332d6367%40%3Cusers.airflow.apache.org%3Emitremailing-listx_refsource_MLIST
- lists.apache.org/thread.html/ra8ce70088ba291f358e077cafdb14d174b7a1ce9a9d86d1b332d6367@%3Cusers.airflow.apache.org%3EghsaWEB
- lists.apache.org/thread.html/rc005f4de9d9b0ba943ceb8ff5a21a5c6ff8a9df52632476698d99432%40%3Cannounce.apache.org%3Emitremailing-listx_refsource_MLIST
- lists.apache.org/thread.html/rc005f4de9d9b0ba943ceb8ff5a21a5c6ff8a9df52632476698d99432@%3Cannounce.apache.org%3EghsaWEB
News mentions
0No linked articles in our index yet.