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