Merge remote-tracking branch 'refs/remotes/tilly-q/variable-front-page'
[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
f1226c98 17import random
f10c3bb8 18import logging
f1226c98 19
62d14bf5 20from werkzeug.exceptions import Forbidden
f1226c98
NY
21from wtforms import Form, HiddenField, validators
22
23from mediagoblin import mg_globals
56dc1c9d 24from mediagoblin.meddleware import BaseMeddleware
947c08ae 25from mediagoblin.tools.translate import lazy_pass_to_ugettext as _
f1226c98 26
f10c3bb8
JW
27_log = logging.getLogger(__name__)
28
f1226c98
NY
29# Use the system (hardware-based) random number generator if it exists.
30# -- this optimization is lifted from Django
31if hasattr(random, 'SystemRandom'):
2dc8d249 32 getrandbits = random.SystemRandom().getrandbits
f1226c98 33else:
2dc8d249 34 getrandbits = random.getrandbits
f1226c98
NY
35
36
ca9ebfe2
NY
37def csrf_exempt(func):
38 """Decorate a Controller to exempt it from CSRF protection."""
39
40 func.csrf_enabled = False
41 return func
42
43
f1226c98
NY
44class CsrfForm(Form):
45 """Simple form to handle rendering a CSRF token and confirming it
46 is included in the POST."""
47
5d2abe45 48 csrf_token = HiddenField("",
f1226c98
NY
49 [validators.Required()])
50
5d2abe45 51
f1226c98
NY
52def render_csrf_form_token(request):
53 """Render the CSRF token in a format suitable for inclusion in a
54 form."""
55
71c6c432
E
56 if 'CSRF_TOKEN' not in request.environ:
57 return None
58
5d2abe45 59 form = CsrfForm(csrf_token=request.environ['CSRF_TOKEN'])
f1226c98
NY
60
61 return form.csrf_token
62
5d2abe45 63
56dc1c9d 64class CsrfMeddleware(BaseMeddleware):
ce5ae8da 65 """CSRF Protection Meddleware
f1226c98
NY
66
67 Adds a CSRF Cookie to responses and verifies that it is present
68 and matches the form token for non-safe requests.
69 """
70
2dc8d249 71 CSRF_KEYLEN = 64
f1226c98
NY
72 SAFE_HTTP_METHODS = ("GET", "HEAD", "OPTIONS", "TRACE")
73
91cf6738 74 def process_request(self, request, controller):
f1226c98
NY
75 """For non-safe requests, confirm that the tokens are present
76 and match.
77 """
78
79 # get the token from the cookie
80 try:
81 request.environ['CSRF_TOKEN'] = \
82 request.cookies[mg_globals.app_config['csrf_cookie_name']]
83
a855e92a 84 except KeyError:
f1226c98
NY
85 # if it doesn't exist, make a new one
86 request.environ['CSRF_TOKEN'] = self._make_token(request)
87
88 # if this is a non-"safe" request (ie, one that could have
89 # side effects), confirm that the CSRF tokens are present and
90 # valid
ca9ebfe2
NY
91 if (getattr(controller, 'csrf_enabled', True) and
92 request.method not in self.SAFE_HTTP_METHODS and
93 ('gmg.verify_csrf' in request.environ or
94 'paste.testing' not in request.environ)
95 ):
7e694e5f 96
f1226c98
NY
97 return self.verify_tokens(request)
98
99 def process_response(self, request, response):
100 """Add the CSRF cookie to the response if needed and set Vary
101 headers.
102 """
103
104 # set the CSRF cookie
105 response.set_cookie(
106 mg_globals.app_config['csrf_cookie_name'],
107 request.environ['CSRF_TOKEN'],
2dc8d249
E
108 path=request.environ['SCRIPT_NAME'],
109 domain=mg_globals.app_config.get('csrf_cookie_domain'),
f1226c98
NY
110 secure=(request.scheme.lower() == 'https'),
111 httponly=True)
112
113 # update the Vary header
53d78991 114 response.vary = list(getattr(response, 'vary', None) or []) + ['Cookie']
f1226c98
NY
115
116 def _make_token(self, request):
117 """Generate a new token to use for CSRF protection."""
118
2dc8d249 119 return "%s" % (getrandbits(self.CSRF_KEYLEN),)
f1226c98
NY
120
121 def verify_tokens(self, request):
122 """Verify that the CSRF Cookie exists and that it matches the
123 form value."""
124
125 # confirm the cookie token was presented
126 cookie_token = request.cookies.get(
5d2abe45 127 mg_globals.app_config['csrf_cookie_name'],
f1226c98
NY
128 None)
129
130 if cookie_token is None:
947c08ae
SS
131 # the CSRF cookie must be present in the request, if not a
132 # cookie blocker might be in action (in the best case)
133 _log.error('CSRF cookie not present')
134 raise Forbidden(_('CSRF cookie not present. This is most likely '
135 'the result of a cookie blocker or somesuch.<br/>'
136 'Make sure to permit the settings of cookies for '
137 'this domain.'))
f1226c98
NY
138
139 # get the form token and confirm it matches
111a609d 140 form = CsrfForm(request.form)
f1226c98
NY
141 if form.validate():
142 form_token = form.csrf_token.data
143
144 if form_token == cookie_token:
145 # all's well that ends well
146 return
147
148 # either the tokens didn't match or the form token wasn't
149 # present; either way, the request is denied
62d14bf5
SS
150 errstr = 'CSRF validation failed'
151 _log.error(errstr)
cfa92229 152 raise Forbidden(errstr)