diff --git a/docs/changelog.rst b/docs/changelog.rst index ee1daaa7..4a137cd3 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -34,10 +34,12 @@ In development - Provide ``WTF_CSRF_FIELD_NAME`` to configure the name of the CSRF token. (`#271`_) - ``CsrfError`` is renamed to ``CSRFError``. (`#271`_) -- ``validate_csrf`` raises ``wtforms.ValidationError`` with specifc messages +- ``validate_csrf`` raises ``wtforms.ValidationError`` with specific messages instead of returning ``True`` or ``False``. This breaks anything that was calling the method directly. (`#239`_, `#271`_) + - CSRF errors are logged as well as raised. (`#239`_) + .. _`#200`: https://github.com/lepture/flask-wtf/issues/200 .. _`#209`: https://github.com/lepture/flask-wtf/pull/209 .. _`#216`: https://github.com/lepture/flask-wtf/issues/216 diff --git a/docs/config.rst b/docs/config.rst index 3f579f9e..683e3b22 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -35,3 +35,10 @@ Recaptcha https://www.google.com/recaptcha/admin/create ``RECAPTCHA_OPTIONS`` **optional** A dict of configuration options. ========================= ============================================== + +Logging +------- + +CSRF errors are logged at the ``INFO`` level to the ``flask_wtf.csrf`` logger. +You still need to configure logging in your application in order to see these +messages. diff --git a/flask_wtf/csrf.py b/flask_wtf/csrf.py index b9ffac42..3533b713 100644 --- a/flask_wtf/csrf.py +++ b/flask_wtf/csrf.py @@ -3,6 +3,7 @@ import warnings from functools import wraps +import logging from flask import Blueprint, current_app, request, session from itsdangerous import BadData, SignatureExpired, URLSafeTimedSerializer from werkzeug.exceptions import BadRequest @@ -13,6 +14,7 @@ from ._compat import FlaskWTFDeprecationWarning, string_types, urlparse __all__ = ('generate_csrf', 'validate_csrf', 'CsrfProtect') +logger = logging.getLogger(__name__) def generate_csrf(secret_key=None, token_key=None): @@ -133,12 +135,16 @@ def generate_csrf_token(self, csrf_token_field): ) def validate_csrf_token(self, form, field): - validate_csrf( - field.data, - self.meta.csrf_secret, - self.meta.csrf_time_limit, - self.meta.csrf_field_name - ) + try: + validate_csrf( + field.data, + self.meta.csrf_secret, + self.meta.csrf_time_limit, + self.meta.csrf_field_name + ) + except ValidationError as e: + logger.info(e.args[0]) + raise class CsrfProtect(object): @@ -238,6 +244,7 @@ def protect(self): try: validate_csrf(self._get_csrf_token()) except ValidationError as e: + logger.info(e.args[0]) self._error_response(e.args[0]) if request.is_secure and current_app.config['WTF_CSRF_SSL_STRICT']: diff --git a/tests/base.py b/tests/base.py index 1097e6b2..70684f0b 100644 --- a/tests/base.py +++ b/tests/base.py @@ -1,7 +1,9 @@ from __future__ import with_statement +from contextlib import contextmanager from unittest import TestCase as _TestCase +import logging from flask import Flask, jsonify, render_template from wtforms import HiddenField, StringField, SubmitField from wtforms.validators import DataRequired @@ -40,6 +42,35 @@ class SimpleForm(FlaskForm): pass +class CaptureHandler(logging.Handler): + def __init__(self): + self.records = [] + logging.Handler.__init__(self, logging.DEBUG) + + def emit(self, record): + self.records.append(record) + + def __iter__(self): + return iter(self.records) + + def __len__(self): + return len(self.records) + + def __getitem__(self, item): + return self.records[item] + + +@contextmanager +def capture_logging(logger): + handler = CaptureHandler() + + try: + logger.addHandler(handler) + yield handler + finally: + logger.removeHandler(handler) + + class TestCase(_TestCase): def setUp(self): self.app = self.create_app() diff --git a/tests/test_validation.py b/tests/test_validation.py index 165e2ed5..b0cee5ea 100644 --- a/tests/test_validation.py +++ b/tests/test_validation.py @@ -3,7 +3,7 @@ from flask import json, request from flask_wtf.csrf import generate_csrf -from .base import MyForm, TestCase, to_unicode +from .base import MyForm, TestCase, to_unicode, capture_logging class TestValidateOnSubmit(TestCase): @@ -60,10 +60,15 @@ def test_csrf_token(self): assert snippet in to_unicode(response.data) def test_invalid_csrf(self): + from flask_wtf.csrf import logger + + with capture_logging(logger) as handler: + response = self.client.post("/", data={"name": "danny"}) - response = self.client.post("/", data={"name": "danny"}) assert b'DANNY' not in response.data assert b'The CSRF token is missing.' in response.data + self.assertEqual(1, len(handler)) + self.assertEqual('The CSRF token is missing.', handler[0].message) def test_csrf_disabled(self):