Commit | Line | Data |
---|---|---|
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 | 17 | import random |
f10c3bb8 | 18 | import logging |
f1226c98 | 19 | |
62d14bf5 | 20 | from werkzeug.exceptions import Forbidden |
f1226c98 NY |
21 | from wtforms import Form, HiddenField, validators |
22 | ||
23 | from mediagoblin import mg_globals | |
56dc1c9d | 24 | from mediagoblin.meddleware import BaseMeddleware |
947c08ae | 25 | from 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 | |
31 | if hasattr(random, 'SystemRandom'): | |
2dc8d249 | 32 | getrandbits = random.SystemRandom().getrandbits |
f1226c98 | 33 | else: |
2dc8d249 | 34 | getrandbits = random.getrandbits |
f1226c98 NY |
35 | |
36 | ||
ca9ebfe2 NY |
37 | def 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 |
44 | class 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("", |
0742e11d | 49 | [validators.InputRequired()]) |
f1226c98 | 50 | |
5d2abe45 | 51 | |
f1226c98 NY |
52 | def 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 | 64 | class 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 ' | |
82c181f0 | 135 | 'the result of a cookie blocker or somesuch. ' |
5acbb79d | 136 | 'Make sure to permit the setting of cookies for ' |
947c08ae | 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) |