|
1 """ |
|
2 Cross Site Request Forgery Middleware. |
|
3 |
|
4 This module provides a middleware that implements protection |
|
5 against request forgeries from other sites. |
|
6 """ |
|
7 |
|
8 import itertools |
|
9 import re |
|
10 import random |
|
11 |
|
12 from django.conf import settings |
|
13 from django.core.urlresolvers import get_callable |
|
14 from django.utils.cache import patch_vary_headers |
|
15 from django.utils.hashcompat import md5_constructor |
|
16 from django.utils.safestring import mark_safe |
|
17 |
|
18 _POST_FORM_RE = \ |
|
19 re.compile(r'(<form\W[^>]*\bmethod\s*=\s*(\'|"|)POST(\'|"|)\b[^>]*>)', re.IGNORECASE) |
|
20 |
|
21 _HTML_TYPES = ('text/html', 'application/xhtml+xml') |
|
22 |
|
23 # Use the system (hardware-based) random number generator if it exists. |
|
24 if hasattr(random, 'SystemRandom'): |
|
25 randrange = random.SystemRandom().randrange |
|
26 else: |
|
27 randrange = random.randrange |
|
28 _MAX_CSRF_KEY = 18446744073709551616L # 2 << 63 |
|
29 |
|
30 def _get_failure_view(): |
|
31 """ |
|
32 Returns the view to be used for CSRF rejections |
|
33 """ |
|
34 return get_callable(settings.CSRF_FAILURE_VIEW) |
|
35 |
|
36 def _get_new_csrf_key(): |
|
37 return md5_constructor("%s%s" |
|
38 % (randrange(0, _MAX_CSRF_KEY), settings.SECRET_KEY)).hexdigest() |
|
39 |
|
40 def _make_legacy_session_token(session_id): |
|
41 return md5_constructor(settings.SECRET_KEY + session_id).hexdigest() |
|
42 |
|
43 def get_token(request): |
|
44 """ |
|
45 Returns the the CSRF token required for a POST form. |
|
46 |
|
47 A side effect of calling this function is to make the the csrf_protect |
|
48 decorator and the CsrfViewMiddleware add a CSRF cookie and a 'Vary: Cookie' |
|
49 header to the outgoing response. For this reason, you may need to use this |
|
50 function lazily, as is done by the csrf context processor. |
|
51 """ |
|
52 request.META["CSRF_COOKIE_USED"] = True |
|
53 return request.META.get("CSRF_COOKIE", None) |
|
54 |
|
55 class CsrfViewMiddleware(object): |
|
56 """ |
|
57 Middleware that requires a present and correct csrfmiddlewaretoken |
|
58 for POST requests that have a CSRF cookie, and sets an outgoing |
|
59 CSRF cookie. |
|
60 |
|
61 This middleware should be used in conjunction with the csrf_token template |
|
62 tag. |
|
63 """ |
|
64 def process_view(self, request, callback, callback_args, callback_kwargs): |
|
65 if getattr(callback, 'csrf_exempt', False): |
|
66 return None |
|
67 |
|
68 if getattr(request, 'csrf_processing_done', False): |
|
69 return None |
|
70 |
|
71 reject = lambda s: _get_failure_view()(request, reason=s) |
|
72 def accept(): |
|
73 # Avoid checking the request twice by adding a custom attribute to |
|
74 # request. This will be relevant when both decorator and middleware |
|
75 # are used. |
|
76 request.csrf_processing_done = True |
|
77 return None |
|
78 |
|
79 # If the user doesn't have a CSRF cookie, generate one and store it in the |
|
80 # request, so it's available to the view. We'll store it in a cookie when |
|
81 # we reach the response. |
|
82 try: |
|
83 request.META["CSRF_COOKIE"] = request.COOKIES[settings.CSRF_COOKIE_NAME] |
|
84 cookie_is_new = False |
|
85 except KeyError: |
|
86 # No cookie, so create one. This will be sent with the next |
|
87 # response. |
|
88 request.META["CSRF_COOKIE"] = _get_new_csrf_key() |
|
89 # Set a flag to allow us to fall back and allow the session id in |
|
90 # place of a CSRF cookie for this request only. |
|
91 cookie_is_new = True |
|
92 |
|
93 if request.method == 'POST': |
|
94 if getattr(request, '_dont_enforce_csrf_checks', False): |
|
95 # Mechanism to turn off CSRF checks for test suite. It comes after |
|
96 # the creation of CSRF cookies, so that everything else continues to |
|
97 # work exactly the same (e.g. cookies are sent etc), but before the |
|
98 # any branches that call reject() |
|
99 return accept() |
|
100 |
|
101 if request.is_ajax(): |
|
102 # .is_ajax() is based on the presence of X-Requested-With. In |
|
103 # the context of a browser, this can only be sent if using |
|
104 # XmlHttpRequest. Browsers implement careful policies for |
|
105 # XmlHttpRequest: |
|
106 # |
|
107 # * Normally, only same-domain requests are allowed. |
|
108 # |
|
109 # * Some browsers (e.g. Firefox 3.5 and later) relax this |
|
110 # carefully: |
|
111 # |
|
112 # * if it is a 'simple' GET or POST request (which can |
|
113 # include no custom headers), it is allowed to be cross |
|
114 # domain. These requests will not be recognized as AJAX. |
|
115 # |
|
116 # * if a 'preflight' check with the server confirms that the |
|
117 # server is expecting and allows the request, cross domain |
|
118 # requests even with custom headers are allowed. These |
|
119 # requests will be recognized as AJAX, but can only get |
|
120 # through when the developer has specifically opted in to |
|
121 # allowing the cross-domain POST request. |
|
122 # |
|
123 # So in all cases, it is safe to allow these requests through. |
|
124 return accept() |
|
125 |
|
126 if request.is_secure(): |
|
127 # Strict referer checking for HTTPS |
|
128 referer = request.META.get('HTTP_REFERER') |
|
129 if referer is None: |
|
130 return reject("Referer checking failed - no Referer.") |
|
131 |
|
132 # The following check ensures that the referer is HTTPS, |
|
133 # the domains match and the ports match. This might be too strict. |
|
134 good_referer = 'https://%s/' % request.get_host() |
|
135 if not referer.startswith(good_referer): |
|
136 return reject("Referer checking failed - %s does not match %s." % |
|
137 (referer, good_referer)) |
|
138 |
|
139 # If the user didn't already have a CSRF cookie, then fall back to |
|
140 # the Django 1.1 method (hash of session ID), so a request is not |
|
141 # rejected if the form was sent to the user before upgrading to the |
|
142 # Django 1.2 method (session independent nonce) |
|
143 if cookie_is_new: |
|
144 try: |
|
145 session_id = request.COOKIES[settings.SESSION_COOKIE_NAME] |
|
146 csrf_token = _make_legacy_session_token(session_id) |
|
147 except KeyError: |
|
148 # No CSRF cookie and no session cookie. For POST requests, |
|
149 # we insist on a CSRF cookie, and in this way we can avoid |
|
150 # all CSRF attacks, including login CSRF. |
|
151 return reject("No CSRF or session cookie.") |
|
152 else: |
|
153 csrf_token = request.META["CSRF_COOKIE"] |
|
154 |
|
155 # check incoming token |
|
156 request_csrf_token = request.POST.get('csrfmiddlewaretoken', None) |
|
157 if request_csrf_token != csrf_token: |
|
158 if cookie_is_new: |
|
159 # probably a problem setting the CSRF cookie |
|
160 return reject("CSRF cookie not set.") |
|
161 else: |
|
162 return reject("CSRF token missing or incorrect.") |
|
163 |
|
164 return accept() |
|
165 |
|
166 def process_response(self, request, response): |
|
167 if getattr(response, 'csrf_processing_done', False): |
|
168 return response |
|
169 |
|
170 # If CSRF_COOKIE is unset, then CsrfViewMiddleware.process_view was |
|
171 # never called, probaby because a request middleware returned a response |
|
172 # (for example, contrib.auth redirecting to a login page). |
|
173 if request.META.get("CSRF_COOKIE") is None: |
|
174 return response |
|
175 |
|
176 if not request.META.get("CSRF_COOKIE_USED", False): |
|
177 return response |
|
178 |
|
179 # Set the CSRF cookie even if it's already set, so we renew the expiry timer. |
|
180 response.set_cookie(settings.CSRF_COOKIE_NAME, |
|
181 request.META["CSRF_COOKIE"], max_age = 60 * 60 * 24 * 7 * 52, |
|
182 domain=settings.CSRF_COOKIE_DOMAIN) |
|
183 # Content varies with the CSRF cookie, so set the Vary header. |
|
184 patch_vary_headers(response, ('Cookie',)) |
|
185 response.csrf_processing_done = True |
|
186 return response |
|
187 |
|
188 class CsrfResponseMiddleware(object): |
|
189 """ |
|
190 DEPRECATED |
|
191 Middleware that post-processes a response to add a csrfmiddlewaretoken. |
|
192 |
|
193 This exists for backwards compatibility and as an interim measure until |
|
194 applications are converted to using use the csrf_token template tag |
|
195 instead. It will be removed in Django 1.4. |
|
196 """ |
|
197 def __init__(self): |
|
198 import warnings |
|
199 warnings.warn( |
|
200 "CsrfResponseMiddleware and CsrfMiddleware are deprecated; use CsrfViewMiddleware and the template tag instead (see CSRF documentation).", |
|
201 PendingDeprecationWarning |
|
202 ) |
|
203 |
|
204 def process_response(self, request, response): |
|
205 if getattr(response, 'csrf_exempt', False): |
|
206 return response |
|
207 |
|
208 if response['Content-Type'].split(';')[0] in _HTML_TYPES: |
|
209 csrf_token = get_token(request) |
|
210 # If csrf_token is None, we have no token for this request, which probably |
|
211 # means that this is a response from a request middleware. |
|
212 if csrf_token is None: |
|
213 return response |
|
214 |
|
215 # ensure we don't add the 'id' attribute twice (HTML validity) |
|
216 idattributes = itertools.chain(("id='csrfmiddlewaretoken'",), |
|
217 itertools.repeat('')) |
|
218 def add_csrf_field(match): |
|
219 """Returns the matched <form> tag plus the added <input> element""" |
|
220 return mark_safe(match.group() + "<div style='display:none;'>" + \ |
|
221 "<input type='hidden' " + idattributes.next() + \ |
|
222 " name='csrfmiddlewaretoken' value='" + csrf_token + \ |
|
223 "' /></div>") |
|
224 |
|
225 # Modify any POST forms |
|
226 response.content, n = _POST_FORM_RE.subn(add_csrf_field, response.content) |
|
227 if n > 0: |
|
228 # Content varies with the CSRF cookie, so set the Vary header. |
|
229 patch_vary_headers(response, ('Cookie',)) |
|
230 |
|
231 # Since the content has been modified, any Etag will now be |
|
232 # incorrect. We could recalculate, but only if we assume that |
|
233 # the Etag was set by CommonMiddleware. The safest thing is just |
|
234 # to delete. See bug #9163 |
|
235 del response['ETag'] |
|
236 return response |
|
237 |
|
238 class CsrfMiddleware(object): |
|
239 """ |
|
240 Django middleware that adds protection against Cross Site |
|
241 Request Forgeries by adding hidden form fields to POST forms and |
|
242 checking requests for the correct value. |
|
243 |
|
244 CsrfMiddleware uses two middleware, CsrfViewMiddleware and |
|
245 CsrfResponseMiddleware, which can be used independently. It is recommended |
|
246 to use only CsrfViewMiddleware and use the csrf_token template tag in |
|
247 templates for inserting the token. |
|
248 """ |
|
249 # We can't just inherit from CsrfViewMiddleware and CsrfResponseMiddleware |
|
250 # because both have process_response methods. |
|
251 def __init__(self): |
|
252 self.response_middleware = CsrfResponseMiddleware() |
|
253 self.view_middleware = CsrfViewMiddleware() |
|
254 |
|
255 def process_response(self, request, resp): |
|
256 # We must do the response post-processing first, because that calls |
|
257 # get_token(), which triggers a flag saying that the CSRF cookie needs |
|
258 # to be sent (done in CsrfViewMiddleware.process_response) |
|
259 resp2 = self.response_middleware.process_response(request, resp) |
|
260 return self.view_middleware.process_response(request, resp2) |
|
261 |
|
262 def process_view(self, request, callback, callback_args, callback_kwargs): |
|
263 return self.view_middleware.process_view(request, callback, callback_args, |
|
264 callback_kwargs) |
|
265 |