Skip to content

Commit 6aca2c2

Browse files
saguzielbolkedebruin
authored andcommitted
[AIRFLOW-836] Use POST and CSRF for state changing endpoints
Closes #2054 from saguziel/aguziel-use-post
1 parent 6613676 commit 6aca2c2

File tree

6 files changed

+73
-21
lines changed

6 files changed

+73
-21
lines changed

airflow/www/templates/admin/master.html

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,14 @@
3838
alert('{{ hostname }}');
3939
});
4040
$('span').tooltip();
41+
42+
$.ajaxSetup({
43+
beforeSend: function(xhr, settings) {
44+
if (!/^(GET|HEAD|OPTIONS|TRACE)$/i.test(settings.type) && !this.crossDomain) {
45+
xhr.setRequestHeader("X-CSRFToken", "{{ csrf_token() }}");
46+
}
47+
}
48+
});
4149
</script>
4250
{% endblock %}
4351

airflow/www/templates/airflow/dag.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ <h3 class="pull-left">
3232
{% if dag.parent_dag %}
3333
<span style='color:#AAA;'>SUBDAG: </span> <span> {{ dag.dag_id }}</span>
3434
{% else %}
35-
<input id="pause_resume" dag_id="{{ dag.dag_id }}" type="checkbox" {{ "checked" if not dag.is_paused else "" }} data-toggle="toggle" data-size="mini">
35+
<input id="pause_resume" dag_id="{{ dag.dag_id }}" type="checkbox" {{ "checked" if not dag.is_paused else "" }} data-toggle="toggle" data-size="mini" method="post">
3636
<span style='color:#AAA;'>DAG: </span> <span> {{ dag.dag_id }}</span> <small class="text-muted"> {{ dag.description }} </small>
3737
{% endif %}
3838
{% if root %}
@@ -364,7 +364,7 @@ <h4 class="modal-title" id="myModalLabel">
364364
is_paused = 'false'
365365
}
366366
url = "{{ url_for('airflow.paused') }}" + '?is_paused=' + is_paused + '&dag_id=' + dag_id;
367-
$.ajax(url);
367+
$.post(url);
368368
});
369369

370370
</script>

airflow/www/templates/airflow/dags.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ <h2>DAGs</h2>
6666
<!-- Column 2: Turn dag on/off -->
6767
<td>
6868
{% if dag_id in orm_dags %}
69-
<input id="toggle-{{ dag_id }}" dag_id="{{ dag_id }}" type="checkbox" {{ "checked" if not orm_dags[dag_id].is_paused else "" }} data-toggle="toggle" data-size="mini">
69+
<input id="toggle-{{ dag_id }}" dag_id="{{ dag_id }}" type="checkbox" {{ "checked" if not orm_dags[dag_id].is_paused else "" }} data-toggle="toggle" data-size="mini" method="post">
7070
{% endif %}
7171
</td>
7272

@@ -214,7 +214,7 @@ <h2>DAGs</h2>
214214
is_paused = 'false'
215215
}
216216
url = 'airflow/paused?is_paused=' + is_paused + '&dag_id=' + dag_id;
217-
$.ajax(url);
217+
$.post(url);
218218
});
219219
});
220220
$('#dags').dataTable({

airflow/www/templates/airflow/query.html

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,12 @@
2929

3030
{% block body %}
3131
<h2>Ad Hoc Query</h2>
32-
<form method="get" id="query_form">
32+
<form method="post" id="query_form">
3333
<div class="form-inline">
3434
<input name="_csrf_token" type="hidden" value="{{ csrf_token() }}">
3535
{{ form.conn_id }}
36-
<input type="submit" class="btn btn-default" value="Run!">
37-
<input type="button" class="btn btn-default" value=".csv" id="csv">
36+
<input type="submit" class="btn btn-default" value="Run!" id="submit_without_csv">
37+
<input type="submit" class="btn btn-default" value=".csv" id="submit_with_csv">
3838
<span id="results"></span><br>
3939
<div id='ace_container'>
4040
{{ form.sql(rows=10) }}
@@ -71,12 +71,16 @@ <h2>Ad Hoc Query</h2>
7171
});
7272
$('select').addClass("form-control");
7373
sync();
74-
$("#query_form").submit(function(event){
74+
$("#submit_without_csv").submit(function(event){
7575
$("#results").html("<img width='25'src='{{ url_for('static', filename='loading.gif') }}'>");
7676
});
77-
$("#csv").on("click", function(){
78-
window.location += '&csv=true';
79-
})
77+
$("#submit_with_csv").click(function(){
78+
$("#csv_value").remove();
79+
$("#query_form").append('<input name="csv" type="hidden" value="true" id="csv_value">');
80+
});
81+
$("#submit_without_csv").click(function(){
82+
$("#csv_value").remove();
83+
});
8084
});
8185
</script>
8286
{% endblock %}

airflow/www/views.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1597,7 +1597,7 @@ def landing_times(self):
15971597
form=form,
15981598
)
15991599

1600-
@expose('/paused')
1600+
@expose('/paused', methods=['POST'])
16011601
@login_required
16021602
@wwwutils.action_logging
16031603
def paused(self):
@@ -1865,7 +1865,7 @@ def index(self):
18651865

18661866

18671867
class QueryView(wwwutils.DataProfilingMixin, BaseView):
1868-
@expose('/')
1868+
@expose('/', methods=['POST', 'GET'])
18691869
@wwwutils.gzipped
18701870
def query(self):
18711871
session = settings.Session()
@@ -1874,9 +1874,9 @@ def query(self):
18741874
session.expunge_all()
18751875
db_choices = list(
18761876
((db.conn_id, db.conn_id) for db in dbs if db.get_hook()))
1877-
conn_id_str = request.args.get('conn_id')
1878-
csv = request.args.get('csv') == "true"
1879-
sql = request.args.get('sql')
1877+
conn_id_str = request.form.get('conn_id')
1878+
csv = request.form.get('csv') == "true"
1879+
sql = request.form.get('sql')
18801880

18811881
class QueryForm(Form):
18821882
conn_id = SelectField("Layout", choices=db_choices)

tests/core.py

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1407,6 +1407,44 @@ def test_variables(self):
14071407
os.remove('variables1.json')
14081408
os.remove('variables2.json')
14091409

1410+
class CSRFTests(unittest.TestCase):
1411+
def setUp(self):
1412+
configuration.load_test_config()
1413+
configuration.conf.set("webserver", "authenticate", "False")
1414+
configuration.conf.set("webserver", "expose_config", "True")
1415+
app = application.create_app()
1416+
app.config['TESTING'] = True
1417+
self.app = app.test_client()
1418+
1419+
self.dagbag = models.DagBag(
1420+
dag_folder=DEV_NULL, include_examples=True)
1421+
self.dag_bash = self.dagbag.dags['example_bash_operator']
1422+
self.runme_0 = self.dag_bash.get_task('runme_0')
1423+
1424+
def get_csrf(self, response):
1425+
tree = html.fromstring(response.data)
1426+
form = tree.find('.//form')
1427+
1428+
return form.find('.//input[@name="_csrf_token"]').value
1429+
1430+
def test_csrf_rejection(self):
1431+
endpoints = ([
1432+
"/admin/queryview/",
1433+
"/admin/airflow/paused?dag_id=example_python_operator&is_paused=false",
1434+
])
1435+
for endpoint in endpoints:
1436+
response = self.app.post(endpoint)
1437+
self.assertIn('CSRF token is missing', response.data.decode('utf-8'))
1438+
1439+
def test_csrf_acceptance(self):
1440+
response = self.app.get("/admin/queryview/")
1441+
csrf = self.get_csrf(response)
1442+
response = self.app.post("/admin/queryview/", data=dict(csrf_token=csrf))
1443+
self.assertEqual(200, response.status_code)
1444+
1445+
def tearDown(self):
1446+
configuration.conf.set("webserver", "expose_config", "False")
1447+
self.dag_bash.clear(start_date=DEFAULT_DATE, end_date=datetime.now())
14101448

14111449
class WebUiTests(unittest.TestCase):
14121450
def setUp(self):
@@ -1415,6 +1453,7 @@ def setUp(self):
14151453
configuration.conf.set("webserver", "expose_config", "True")
14161454
app = application.create_app()
14171455
app.config['TESTING'] = True
1456+
app.config['WTF_CSRF_METHODS'] = []
14181457
self.app = app.test_client()
14191458

14201459
self.dagbag = models.DagBag(include_examples=True)
@@ -1445,10 +1484,10 @@ def test_index(self):
14451484
def test_query(self):
14461485
response = self.app.get('/admin/queryview/')
14471486
self.assertIn("Ad Hoc Query", response.data.decode('utf-8'))
1448-
response = self.app.get(
1449-
"/admin/queryview/?"
1450-
"conn_id=airflow_db&"
1451-
"sql=SELECT+COUNT%281%29+as+TEST+FROM+task_instance")
1487+
response = self.app.post(
1488+
"/admin/queryview/", data=dict(
1489+
conn_id="airflow_db",
1490+
sql="SELECT+COUNT%281%29+as+TEST+FROM+task_instance"))
14521491
self.assertIn("TEST", response.data.decode('utf-8'))
14531492

14541493
def test_health(self):
@@ -1563,9 +1602,10 @@ def test_dag_views(self):
15631602
response = self.app.get(
15641603
"/admin/airflow/refresh?dag_id=example_bash_operator")
15651604
response = self.app.get("/admin/airflow/refresh_all")
1566-
response = self.app.get(
1605+
response = self.app.post(
15671606
"/admin/airflow/paused?"
15681607
"dag_id=example_python_operator&is_paused=false")
1608+
self.assertIn("OK", response.data.decode('utf-8'))
15691609
response = self.app.get("/admin/xcom", follow_redirects=True)
15701610
self.assertIn("Xcoms", response.data.decode('utf-8'))
15711611

0 commit comments

Comments
 (0)