Issue 361 Initial implementation of CSRF protection middleware
authorNathan Yergler <nathan@yergler.net>
Mon, 5 Sep 2011 01:15:52 +0000 (18:15 -0700)
committerNathan Yergler <nathan@yergler.net>
Mon, 5 Sep 2011 01:15:52 +0000 (18:15 -0700)
mediagoblin/config_spec.ini
mediagoblin/middleware/__init__.py
mediagoblin/middleware/csrf.py [new file with mode: 0644]
mediagoblin/util.py

index a0fbde099e7137f350d56298ba9708e748f3995c..8018b243a70742508fc99280d5141a2eeafb06e1 100644 (file)
@@ -41,6 +41,9 @@ celery_setup_elsewhere = boolean(default=False)
 # source files for a media file but can also be a HUGE security risk.
 allow_attachments = boolean(default=False)
 
+# Cookie stuff
+secret_key = string(default="Something Super Duper Secrit!")
+csrf_cookie_name = string(default='mediagoblin_nonce')
 
 [storage:publicstore]
 base_dir = string(default="%(here)s/user_dev/media/public")
index 586debbf974682a8a440f7523aaa9b0e4d8aa4ec..05325ee52c089e3232b802299a33aef24d1f787c 100644 (file)
@@ -16,4 +16,5 @@
 
 ENABLED_MIDDLEWARE = (
     'mediagoblin.middleware.noop:NoOpMiddleware',
+    'mediagoblin.middleware.csrf:CsrfMiddleware',
     )
diff --git a/mediagoblin/middleware/csrf.py b/mediagoblin/middleware/csrf.py
new file mode 100644 (file)
index 0000000..a372d0b
--- /dev/null
@@ -0,0 +1,131 @@
+# GNU MediaGoblin -- federated, autonomous media hosting
+# Copyright (C) 2011 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 hashlib
+import random
+
+from webob.exc import HTTPForbidden
+from wtforms import Form, HiddenField, validators
+
+from mediagoblin import mg_globals
+
+# Use the system (hardware-based) random number generator if it exists.
+# -- this optimization is lifted from Django
+if hasattr(random, 'SystemRandom'):
+    randrange = random.SystemRandom().randrange
+else:
+    randrange = random.randrange
+
+
+class CsrfForm(Form):
+    """Simple form to handle rendering a CSRF token and confirming it
+    is included in the POST."""
+
+    csrf_token = HiddenField("", 
+                             [validators.Required()])
+
+def render_csrf_form_token(request):
+    """Render the CSRF token in a format suitable for inclusion in a
+    form."""
+
+    form = CsrfForm(csrf_token = request.environ['CSRF_TOKEN'])
+
+    return form.csrf_token
+
+class CsrfMiddleware(object):
+    """CSRF Protection Middleware
+
+    Adds a CSRF Cookie to responses and verifies that it is present
+    and matches the form token for non-safe requests.
+    """
+
+    MAX_CSRF_KEY = 2 << 63
+    SAFE_HTTP_METHODS = ("GET", "HEAD", "OPTIONS", "TRACE")
+
+    def __init__(self, mg_app):
+        self.app = mg_app
+
+    def process_request(self, request):
+        """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, e:
+            # 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 request.method not in self.SAFE_HTTP_METHODS:
+            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'],
+            max_age=60*60*24*7*52, path='/',
+            domain=mg_globals.app_config.get('csrf_cookie_domain', None),
+            secure=(request.scheme.lower() == 'https'),
+            httponly=True)
+
+        # update the Vary header
+        response.vary = (response.vary or []) + ['Cookie']
+
+    def _make_token(self, request):
+        """Generate a new token to use for CSRF protection."""
+
+        return hashlib.md5("%s%s" % 
+                           (randrange(0, self.MAX_CSRF_KEY), 
+                            mg_globals.app_config['secret_key'])
+                           ).hexdigest()
+
+    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
+            return HTTPForbidden()
+
+        # get the form token and confirm it matches
+        form = CsrfForm(request.POST)
+        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
+        return HTTPForbidden()
+
index e391b8b0b1eaeaa6cea6d6289a962d637f1814fd..bc72f8dfe9bc505e03a12dc1403c5b20b4fea99a 100644 (file)
@@ -39,6 +39,7 @@ from wtforms.form import Form
 from mediagoblin import mg_globals
 from mediagoblin import messages
 from mediagoblin.db.util import ObjectId
+from mediagoblin.middleware.csrf import render_csrf_form_token
 
 from itertools import izip, count
 
@@ -125,6 +126,8 @@ def render_template(request, template_path, context):
     template = request.template_env.get_template(
         template_path)
     context['request'] = request
+    context['csrf_token'] = render_csrf_form_token(request)
+
     rendered = template.render(context)
 
     if TESTS_ENABLED: