123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153 |
- # GNU MediaGoblin -- federated, autonomous media hosting
- # Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS.
- #
- # This program is free software: you can redistribute it and/or modify
- # it under the terms of the GNU Affero General Public License as published by
- # the Free Software Foundation, either version 3 of the License, or
- # (at your option) any later version.
- #
- # This program is distributed in the hope that it will be useful,
- # but WITHOUT ANY WARRANTY; without even the implied warranty of
- # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- # GNU Affero General Public License for more details.
- #
- # You should have received a copy of the GNU Affero General Public License
- # along with this program. If not, see <http://www.gnu.org/licenses/>.
- import random
- import logging
- from werkzeug.exceptions import Forbidden
- from wtforms import Form, HiddenField, validators
- from mediagoblin import mg_globals
- from mediagoblin.meddleware import BaseMeddleware
- from mediagoblin.tools.translate import lazy_pass_to_ugettext as _
- _log = logging.getLogger(__name__)
- # Use the system (hardware-based) random number generator if it exists.
- # -- this optimization is lifted from Django
- if hasattr(random, 'SystemRandom'):
- getrandbits = random.SystemRandom().getrandbits
- else:
- getrandbits = random.getrandbits
- def csrf_exempt(func):
- """Decorate a Controller to exempt it from CSRF protection."""
- func.csrf_enabled = False
- return func
- class CsrfForm(Form):
- """Simple form to handle rendering a CSRF token and confirming it
- is included in the POST."""
- csrf_token = HiddenField("",
- [validators.InputRequired()])
- def render_csrf_form_token(request):
- """Render the CSRF token in a format suitable for inclusion in a
- form."""
- if 'CSRF_TOKEN' not in request.environ:
- return None
- form = CsrfForm(csrf_token=request.environ['CSRF_TOKEN'])
- return form.csrf_token
- class CsrfMeddleware(BaseMeddleware):
- """CSRF Protection Meddleware
- Adds a CSRF Cookie to responses and verifies that it is present
- and matches the form token for non-safe requests.
- """
- CSRF_KEYLEN = 64
- SAFE_HTTP_METHODS = ("GET", "HEAD", "OPTIONS", "TRACE")
- def process_request(self, request, controller):
- """For non-safe requests, confirm that the tokens are present
- and match.
- """
- # get the token from the cookie
- try:
- request.environ['CSRF_TOKEN'] = \
- request.cookies[mg_globals.app_config['csrf_cookie_name']]
- except KeyError:
- # if it doesn't exist, make a new one
- request.environ['CSRF_TOKEN'] = self._make_token(request)
- # if this is a non-"safe" request (ie, one that could have
- # side effects), confirm that the CSRF tokens are present and
- # valid
- if (getattr(controller, 'csrf_enabled', True) and
- request.method not in self.SAFE_HTTP_METHODS and
- ('gmg.verify_csrf' in request.environ or
- 'paste.testing' not in request.environ)
- ):
- return self.verify_tokens(request)
- def process_response(self, request, response):
- """Add the CSRF cookie to the response if needed and set Vary
- headers.
- """
- # set the CSRF cookie
- response.set_cookie(
- mg_globals.app_config['csrf_cookie_name'],
- request.environ['CSRF_TOKEN'],
- path=request.environ['SCRIPT_NAME'],
- domain=mg_globals.app_config.get('csrf_cookie_domain'),
- secure=(request.scheme.lower() == 'https'),
- httponly=True)
- # update the Vary header
- response.vary = list(getattr(response, 'vary', None) or []) + ['Cookie']
- def _make_token(self, request):
- """Generate a new token to use for CSRF protection."""
- return "%s" % (getrandbits(self.CSRF_KEYLEN),)
- def verify_tokens(self, request):
- """Verify that the CSRF Cookie exists and that it matches the
- form value."""
- # confirm the cookie token was presented
- cookie_token = request.cookies.get(
- mg_globals.app_config['csrf_cookie_name'],
- None)
- if cookie_token is None:
- # the CSRF cookie must be present in the request, if not a
- # cookie blocker might be in action (in the best case)
- _log.error('CSRF cookie not present')
- raise Forbidden(_('CSRF cookie not present. This is most likely '
- 'the result of a cookie blocker or somesuch.<br/>'
- 'Make sure to permit the settings of cookies for '
- 'this domain.'))
- # get the form token and confirm it matches
- form = CsrfForm(request.form)
- if form.validate():
- form_token = form.csrf_token.data
- if form_token == cookie_token:
- # all's well that ends well
- return
- # either the tokens didn't match or the form token wasn't
- # present; either way, the request is denied
- errstr = 'CSRF validation failed'
- _log.error(errstr)
- raise Forbidden(errstr)
|