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