From e69f991aa6d7eb36e12562287d3601c4fbd5ee6a Mon Sep 17 00:00:00 2001 From: Francesco Siddi Date: Wed, 28 Mar 2018 22:05:54 +0200 Subject: [PATCH] Update flask_wtf to 0.14.2 and make CSRFProtect available to current_app By default CSRF protection is disabled for all views, since most web endpoints and all API endpoints do not need it. On the views that require it, we use the current_app.csrf.protect() method. --- pillar/__init__.py | 5 +++++ pillar/config.py | 5 +++++ pillar/web/nodes/routes.py | 5 ++--- requirements.txt | 2 +- setup.py | 2 +- src/templates/layout.pug | 11 ++++++++++- 6 files changed, 24 insertions(+), 6 deletions(-) diff --git a/pillar/__init__.py b/pillar/__init__.py index f2e9e713..c6259ba7 100644 --- a/pillar/__init__.py +++ b/pillar/__init__.py @@ -73,6 +73,7 @@ class PillarServer(BlinkerCompatibleEve): def __init__(self, app_root, **kwargs): from .extension import PillarExtension from celery import Celery + from flask_wtf.csrf import CSRFProtect kwargs.setdefault('validator', custom_field_validation.ValidateCustomFields) super(PillarServer, self).__init__(settings=empty_settings, **kwargs) @@ -141,6 +142,10 @@ class PillarServer(BlinkerCompatibleEve): self.before_first_request(self.setup_db_indices) + # Make CSRF protection available to the application. By default it is + # disabled on all endpoints. More info at WTF_CSRF_CHECK_DEFAULT in config.py + self.csrf = CSRFProtect(self) + def _validate_config(self): if not self.config.get('SECRET_KEY'): raise ConfigurationMissingError('SECRET_KEY configuration key is missing') diff --git a/pillar/config.py b/pillar/config.py index 9c2f49a1..6f0b9d87 100644 --- a/pillar/config.py +++ b/pillar/config.py @@ -255,3 +255,8 @@ SEND_FILE_MAX_AGE_DEFAULT = 3600 * 24 * 365 # seconds # be used. Note that this causes extra traffic, since every time the process # restarts the URLs will be different. STATIC_FILE_HASH = '' + +# Disable default CSRF protection for all views, since most web endpoints and +# all API endpoints do not need it. On the views that require it, we use the +# current_app.csrf.protect() method. +WTF_CSRF_CHECK_DEFAULT = False diff --git a/pillar/web/nodes/routes.py b/pillar/web/nodes/routes.py index 105ca1dd..62067738 100644 --- a/pillar/web/nodes/routes.py +++ b/pillar/web/nodes/routes.py @@ -487,9 +487,8 @@ def preview_markdown(): content of a node. """ - if not validate_csrf(request.headers.get('X-CSRFToken')): - return jsonify({'_status': 'ERR', - 'message': 'CSRF validation failed.'}), 403 + current_app.csrf.protect() + try: content = request.form['content'] except KeyError: diff --git a/requirements.txt b/requirements.txt index 33c16809..b9ba4b8d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -16,7 +16,7 @@ Flask-Babel==0.11.2 Flask-Cache==0.13.1 Flask-Script==2.0.6 Flask-Login==0.3.2 -Flask-WTF==0.12 +Flask-WTF==0.14.2 gcloud==0.12.0 google-apitools==0.4.11 httplib2==0.9.2 diff --git a/setup.py b/setup.py index 1d1014ea..577f5119 100644 --- a/setup.py +++ b/setup.py @@ -39,7 +39,7 @@ setuptools.setup( 'Flask-Script>=2.0.5', 'Flask-Login>=0.3.2', 'Flask-OAuthlib>=0.9.3', - 'Flask-WTF>=0.12', + 'Flask-WTF>=0.14.2', 'algoliasearch>=1.12.0', # Limit the major version to the major version of ElasticSearch we're using. diff --git a/src/templates/layout.pug b/src/templates/layout.pug index e8f439e0..b1f5f6f9 100644 --- a/src/templates/layout.pug +++ b/src/templates/layout.pug @@ -87,7 +87,16 @@ html(lang="en") | {% block footer_scripts_pre %}{% endblock %} | {% block footer_scripts %}{% endblock %} - + script. + // When sending an AJAX request, always add the X-CSRFToken header to it. + var csrf_token = "{{ csrf_token() }}"; + $.ajaxSetup({ + beforeSend: function (xhr, settings) { + if (!/^(GET|HEAD|OPTIONS|TRACE)$/i.test(settings.type) && !this.crossDomain) { + xhr.setRequestHeader("X-CSRFToken", csrf_token); + } + } + }); script. (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){