1 # GNU MediaGoblin -- federated, autonomous media hosting
2 # Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS.
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.
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.
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/>.
20 from werkzeug
.exceptions
import Forbidden
21 from wtforms
import Form
, HiddenField
, validators
23 from mediagoblin
import mg_globals
24 from mediagoblin
.meddleware
import BaseMeddleware
26 _log
= logging
.getLogger(__name__
)
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
33 getrandbits
= random
.getrandbits
36 def csrf_exempt(func
):
37 """Decorate a Controller to exempt it from CSRF protection."""
39 func
.csrf_enabled
= False
44 """Simple form to handle rendering a CSRF token and confirming it
45 is included in the POST."""
47 csrf_token
= HiddenField("",
48 [validators
.Required()])
51 def render_csrf_form_token(request
):
52 """Render the CSRF token in a format suitable for inclusion in a
55 if 'CSRF_TOKEN' not in request
.environ
:
58 form
= CsrfForm(csrf_token
=request
.environ
['CSRF_TOKEN'])
60 return form
.csrf_token
63 class CsrfMeddleware(BaseMeddleware
):
64 """CSRF Protection Meddleware
66 Adds a CSRF Cookie to responses and verifies that it is present
67 and matches the form token for non-safe requests.
71 SAFE_HTTP_METHODS
= ("GET", "HEAD", "OPTIONS", "TRACE")
73 def process_request(self
, request
, controller
):
74 """For non-safe requests, confirm that the tokens are present
78 # get the token from the cookie
80 request
.environ
['CSRF_TOKEN'] = \
81 request
.cookies
[mg_globals
.app_config
['csrf_cookie_name']]
84 # if it doesn't exist, make a new one
85 request
.environ
['CSRF_TOKEN'] = self
._make
_token
(request
)
87 # if this is a non-"safe" request (ie, one that could have
88 # side effects), confirm that the CSRF tokens are present and
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
)
96 return self
.verify_tokens(request
)
98 def process_response(self
, request
, response
):
99 """Add the CSRF cookie to the response if needed and set Vary
103 # set the CSRF 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'),
112 # update the Vary header
113 response
.vary
= (getattr(response
, 'vary', None) or []) + ['Cookie']
115 def _make_token(self
, request
):
116 """Generate a new token to use for CSRF protection."""
118 return "%s" % (getrandbits(self
.CSRF_KEYLEN
),)
120 def verify_tokens(self
, request
):
121 """Verify that the CSRF Cookie exists and that it matches the
124 # confirm the cookie token was presented
125 cookie_token
= request
.cookies
.get(
126 mg_globals
.app_config
['csrf_cookie_name'],
129 if cookie_token
is None:
130 # the CSRF cookie must be present in the request
131 errstr
= 'CSRF cookie not present'
133 raise Forbidden(errstr
)
135 # get the form token and confirm it matches
136 form
= CsrfForm(request
.form
)
138 form_token
= form
.csrf_token
.data
140 if form_token
== cookie_token
:
141 # all's well that ends well
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'
148 raise Forbidden(errstr
)