It's 2012 all up in here
[mediagoblin.git] / mediagoblin / meddleware / csrf.py
CommitLineData
f1226c98 1# GNU MediaGoblin -- federated, autonomous media hosting
cf29e8a8 2# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS.
f1226c98
NY
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
56dc1c9d 24from mediagoblin.meddleware import BaseMeddleware
f1226c98
NY
25
26# Use the system (hardware-based) random number generator if it exists.
27# -- this optimization is lifted from Django
28if hasattr(random, 'SystemRandom'):
2dc8d249 29 getrandbits = random.SystemRandom().getrandbits
f1226c98 30else:
2dc8d249 31 getrandbits = random.getrandbits
f1226c98
NY
32
33
ca9ebfe2
NY
34def csrf_exempt(func):
35 """Decorate a Controller to exempt it from CSRF protection."""
36
37 func.csrf_enabled = False
38 return func
39
40
f1226c98
NY
41class CsrfForm(Form):
42 """Simple form to handle rendering a CSRF token and confirming it
43 is included in the POST."""
44
5d2abe45 45 csrf_token = HiddenField("",
f1226c98
NY
46 [validators.Required()])
47
5d2abe45 48
f1226c98
NY
49def render_csrf_form_token(request):
50 """Render the CSRF token in a format suitable for inclusion in a
51 form."""
52
71c6c432
E
53 if 'CSRF_TOKEN' not in request.environ:
54 return None
55
5d2abe45 56 form = CsrfForm(csrf_token=request.environ['CSRF_TOKEN'])
f1226c98
NY
57
58 return form.csrf_token
59
5d2abe45 60
56dc1c9d 61class CsrfMeddleware(BaseMeddleware):
ce5ae8da 62 """CSRF Protection Meddleware
f1226c98
NY
63
64 Adds a CSRF Cookie to responses and verifies that it is present
65 and matches the form token for non-safe requests.
66 """
67
2dc8d249 68 CSRF_KEYLEN = 64
f1226c98
NY
69 SAFE_HTTP_METHODS = ("GET", "HEAD", "OPTIONS", "TRACE")
70
91cf6738 71 def process_request(self, request, controller):
f1226c98
NY
72 """For non-safe requests, confirm that the tokens are present
73 and match.
74 """
75
76 # get the token from the cookie
77 try:
78 request.environ['CSRF_TOKEN'] = \
79 request.cookies[mg_globals.app_config['csrf_cookie_name']]
80
81 except KeyError, e:
82 # if it doesn't exist, make a new one
83 request.environ['CSRF_TOKEN'] = self._make_token(request)
84
85 # if this is a non-"safe" request (ie, one that could have
86 # side effects), confirm that the CSRF tokens are present and
87 # valid
ca9ebfe2
NY
88 if (getattr(controller, 'csrf_enabled', True) and
89 request.method not in self.SAFE_HTTP_METHODS and
90 ('gmg.verify_csrf' in request.environ or
91 'paste.testing' not in request.environ)
92 ):
7e694e5f 93
f1226c98
NY
94 return self.verify_tokens(request)
95
96 def process_response(self, request, response):
97 """Add the CSRF cookie to the response if needed and set Vary
98 headers.
99 """
100
101 # set the CSRF cookie
102 response.set_cookie(
103 mg_globals.app_config['csrf_cookie_name'],
104 request.environ['CSRF_TOKEN'],
2dc8d249
E
105 path=request.environ['SCRIPT_NAME'],
106 domain=mg_globals.app_config.get('csrf_cookie_domain'),
f1226c98
NY
107 secure=(request.scheme.lower() == 'https'),
108 httponly=True)
109
110 # update the Vary header
d9ed3aeb 111 response.vary = (getattr(response, 'vary', None) or []) + ['Cookie']
f1226c98
NY
112
113 def _make_token(self, request):
114 """Generate a new token to use for CSRF protection."""
115
2dc8d249 116 return "%s" % (getrandbits(self.CSRF_KEYLEN),)
f1226c98
NY
117
118 def verify_tokens(self, request):
119 """Verify that the CSRF Cookie exists and that it matches the
120 form value."""
121
122 # confirm the cookie token was presented
123 cookie_token = request.cookies.get(
5d2abe45 124 mg_globals.app_config['csrf_cookie_name'],
f1226c98
NY
125 None)
126
127 if cookie_token is None:
128 # the CSRF cookie must be present in the request
129 return HTTPForbidden()
130
131 # get the form token and confirm it matches
132 form = CsrfForm(request.POST)
133 if form.validate():
134 form_token = form.csrf_token.data
135
136 if form_token == cookie_token:
137 # all's well that ends well
138 return
139
140 # either the tokens didn't match or the form token wasn't
141 # present; either way, the request is denied
142 return HTTPForbidden()