Uncommenting requires=['gst'] till I figure out why Joar added it there :)
[mediagoblin.git] / mediagoblin / meddleware / csrf.py
CommitLineData
f1226c98
NY
1# GNU MediaGoblin -- federated, autonomous media hosting
2# Copyright (C) 2011 MediaGoblin contributors. See AUTHORS.
3#
4# This program is free software: you can redistribute it and/or modify
5# it under the terms of the GNU Affero General Public License as published by
6# the Free Software Foundation, either version 3 of the License, or
7# (at your option) any later version.
8#
9# This program is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Affero General Public License for more details.
13#
14# You should have received a copy of the GNU Affero General Public License
15# along with this program. If not, see <http://www.gnu.org/licenses/>.
16
17import hashlib
18import random
19
20from webob.exc import HTTPForbidden
21from wtforms import Form, HiddenField, validators
22
23from mediagoblin import mg_globals
24
25# Use the system (hardware-based) random number generator if it exists.
26# -- this optimization is lifted from Django
27if hasattr(random, 'SystemRandom'):
2dc8d249 28 getrandbits = random.SystemRandom().getrandbits
f1226c98 29else:
2dc8d249 30 getrandbits = random.getrandbits
f1226c98
NY
31
32
33class CsrfForm(Form):
34 """Simple form to handle rendering a CSRF token and confirming it
35 is included in the POST."""
36
5d2abe45 37 csrf_token = HiddenField("",
f1226c98
NY
38 [validators.Required()])
39
5d2abe45 40
f1226c98
NY
41def render_csrf_form_token(request):
42 """Render the CSRF token in a format suitable for inclusion in a
43 form."""
44
5d2abe45 45 form = CsrfForm(csrf_token=request.environ['CSRF_TOKEN'])
f1226c98
NY
46
47 return form.csrf_token
48
5d2abe45 49
ce5ae8da
CAW
50class CsrfMeddleware(object):
51 """CSRF Protection Meddleware
f1226c98
NY
52
53 Adds a CSRF Cookie to responses and verifies that it is present
54 and matches the form token for non-safe requests.
55 """
56
2dc8d249 57 CSRF_KEYLEN = 64
f1226c98
NY
58 SAFE_HTTP_METHODS = ("GET", "HEAD", "OPTIONS", "TRACE")
59
60 def __init__(self, mg_app):
61 self.app = mg_app
62
63 def process_request(self, request):
64 """For non-safe requests, confirm that the tokens are present
65 and match.
66 """
67
68 # get the token from the cookie
69 try:
70 request.environ['CSRF_TOKEN'] = \
71 request.cookies[mg_globals.app_config['csrf_cookie_name']]
72
73 except KeyError, e:
74 # if it doesn't exist, make a new one
75 request.environ['CSRF_TOKEN'] = self._make_token(request)
76
77 # if this is a non-"safe" request (ie, one that could have
78 # side effects), confirm that the CSRF tokens are present and
79 # valid
7e694e5f
NY
80 if request.method not in self.SAFE_HTTP_METHODS \
81 and ('gmg.verify_csrf' in request.environ or
82 'paste.testing' not in request.environ):
83
f1226c98
NY
84 return self.verify_tokens(request)
85
86 def process_response(self, request, response):
87 """Add the CSRF cookie to the response if needed and set Vary
88 headers.
89 """
90
91 # set the CSRF cookie
92 response.set_cookie(
93 mg_globals.app_config['csrf_cookie_name'],
94 request.environ['CSRF_TOKEN'],
2dc8d249
E
95 path=request.environ['SCRIPT_NAME'],
96 domain=mg_globals.app_config.get('csrf_cookie_domain'),
f1226c98
NY
97 secure=(request.scheme.lower() == 'https'),
98 httponly=True)
99
100 # update the Vary header
d9ed3aeb 101 response.vary = (getattr(response, 'vary', None) or []) + ['Cookie']
f1226c98
NY
102
103 def _make_token(self, request):
104 """Generate a new token to use for CSRF protection."""
105
2dc8d249 106 return "%s" % (getrandbits(self.CSRF_KEYLEN),)
f1226c98
NY
107
108 def verify_tokens(self, request):
109 """Verify that the CSRF Cookie exists and that it matches the
110 form value."""
111
112 # confirm the cookie token was presented
113 cookie_token = request.cookies.get(
5d2abe45 114 mg_globals.app_config['csrf_cookie_name'],
f1226c98
NY
115 None)
116
117 if cookie_token is None:
118 # the CSRF cookie must be present in the request
119 return HTTPForbidden()
120
121 # get the form token and confirm it matches
122 form = CsrfForm(request.POST)
123 if form.validate():
124 form_token = form.csrf_token.data
125
126 if form_token == cookie_token:
127 # all's well that ends well
128 return
129
130 # either the tokens didn't match or the form token wasn't
131 # present; either way, the request is denied
132 return HTTPForbidden()