VYPR
Moderate severityNVD Advisory· Published Sep 17, 2020· Updated Aug 4, 2024

CVE-2020-13944

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.

PackageAffected versionsPatched versions
apache-airflowPyPI
< 1.10.121.10.12

Affected products

3

Patches

1
5c2bb7b0b0e7

Webserver: Sanitize values passed to origin param (#10334)

https://github.com/apache/airflowKaxil NaikAug 15, 2020via ghsa
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

News mentions

0

No linked articles in our index yet.