diff options
author | Christopher Allan Webber <cwebber@dustycloud.org> | 2011-11-25 12:13:56 -0600 |
---|---|---|
committer | Christopher Allan Webber <cwebber@dustycloud.org> | 2011-11-25 12:13:56 -0600 |
commit | ce5ae8da19707019cd62d42533e591d71071f4fe (patch) | |
tree | 19a14eb016865447292f4f5f298d6f3d337596d6 /mediagoblin/meddleware | |
parent | 4da6efb45956321d831339da0fcdbbb6553c6846 (diff) | |
download | mediagoblin-ce5ae8da19707019cd62d42533e591d71071f4fe.tar.lz mediagoblin-ce5ae8da19707019cd62d42533e591d71071f4fe.tar.xz mediagoblin-ce5ae8da19707019cd62d42533e591d71071f4fe.zip |
Rename MediaGoblin middleware to meddleware to avoid confusion w/ wsgi middleware
hehehehehe, "meddleware"
Diffstat (limited to 'mediagoblin/meddleware')
-rw-r--r-- | mediagoblin/meddleware/__init__.py | 20 | ||||
-rw-r--r-- | mediagoblin/meddleware/csrf.py | 132 | ||||
-rw-r--r-- | mediagoblin/meddleware/noop.py | 27 |
3 files changed, 179 insertions, 0 deletions
diff --git a/mediagoblin/meddleware/__init__.py b/mediagoblin/meddleware/__init__.py new file mode 100644 index 00000000..3addc937 --- /dev/null +++ b/mediagoblin/meddleware/__init__.py @@ -0,0 +1,20 @@ +# 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/>. + +ENABLED_MEDDLEWARE = ( + 'mediagoblin.meddleware.noop:NoOpMeddleware', + 'mediagoblin.meddleware.csrf:CsrfMeddleware', + ) diff --git a/mediagoblin/meddleware/csrf.py b/mediagoblin/meddleware/csrf.py new file mode 100644 index 00000000..051afe58 --- /dev/null +++ b/mediagoblin/meddleware/csrf.py @@ -0,0 +1,132 @@ +# 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'): + getrandbits = random.SystemRandom().getrandbits +else: + getrandbits = random.getrandbits + + +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 CsrfMeddleware(object): + """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 __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 \ + 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 = (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 + 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() diff --git a/mediagoblin/meddleware/noop.py b/mediagoblin/meddleware/noop.py new file mode 100644 index 00000000..d11a5b9e --- /dev/null +++ b/mediagoblin/meddleware/noop.py @@ -0,0 +1,27 @@ +# 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/>. + + +class NoOpMeddleware(object): + + def __init__(self, mg_app): + self.app = mg_app + + def process_request(self, request): + pass + + def process_response(self, request, response): + pass |