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 | ||
17 | import hashlib | |
18 | import random | |
19 | ||
20 | from webob.exc import HTTPForbidden | |
21 | from wtforms import Form, HiddenField, validators | |
22 | ||
23 | from mediagoblin import mg_globals | |
56dc1c9d | 24 | from 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 | |
28 | if hasattr(random, 'SystemRandom'): | |
2dc8d249 | 29 | getrandbits = random.SystemRandom().getrandbits |
f1226c98 | 30 | else: |
2dc8d249 | 31 | getrandbits = random.getrandbits |
f1226c98 NY |
32 | |
33 | ||
ca9ebfe2 NY |
34 | def 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 |
41 | class 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 |
49 | def 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 | 61 | class 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() |