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(){