Skip to content

HTTPS clone URL

Subversion checkout URL

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