Coverage for /var/srv/projects/api.amasfac.comuna18.com/tmp/venv/lib/python3.9/site-packages/django/middleware/csrf.py: 28%
225 statements
« prev ^ index » next coverage.py v6.4.4, created at 2023-07-17 14:22 -0600
« prev ^ index » next coverage.py v6.4.4, created at 2023-07-17 14:22 -0600
1"""
2Cross Site Request Forgery Middleware.
4This module provides a middleware that implements protection
5against request forgeries from other sites.
6"""
7import logging
8import string
9from collections import defaultdict
10from urllib.parse import urlparse
12from django.conf import settings
13from django.core.exceptions import DisallowedHost, ImproperlyConfigured
14from django.http import UnreadablePostError
15from django.http.request import HttpHeaders
16from django.urls import get_callable
17from django.utils.cache import patch_vary_headers
18from django.utils.crypto import constant_time_compare, get_random_string
19from django.utils.deprecation import MiddlewareMixin
20from django.utils.functional import cached_property
21from django.utils.http import is_same_domain
22from django.utils.log import log_response
23from django.utils.regex_helper import _lazy_re_compile
25logger = logging.getLogger("django.security.csrf")
26# This matches if any character is not in CSRF_ALLOWED_CHARS.
27invalid_token_chars_re = _lazy_re_compile("[^a-zA-Z0-9]")
29REASON_BAD_ORIGIN = "Origin checking failed - %s does not match any trusted origins."
30REASON_NO_REFERER = "Referer checking failed - no Referer."
31REASON_BAD_REFERER = "Referer checking failed - %s does not match any trusted origins."
32REASON_NO_CSRF_COOKIE = "CSRF cookie not set."
33REASON_CSRF_TOKEN_MISSING = "CSRF token missing."
34REASON_MALFORMED_REFERER = "Referer checking failed - Referer is malformed."
35REASON_INSECURE_REFERER = (
36 "Referer checking failed - Referer is insecure while host is secure."
37)
38# The reason strings below are for passing to InvalidTokenFormat. They are
39# phrases without a subject because they can be in reference to either the CSRF
40# cookie or non-cookie token.
41REASON_INCORRECT_LENGTH = "has incorrect length"
42REASON_INVALID_CHARACTERS = "has invalid characters"
44CSRF_SECRET_LENGTH = 32
45CSRF_TOKEN_LENGTH = 2 * CSRF_SECRET_LENGTH
46CSRF_ALLOWED_CHARS = string.ascii_letters + string.digits
47CSRF_SESSION_KEY = "_csrftoken"
50def _get_failure_view():
51 """Return the view to be used for CSRF rejections."""
52 return get_callable(settings.CSRF_FAILURE_VIEW)
55def _get_new_csrf_string():
56 return get_random_string(CSRF_SECRET_LENGTH, allowed_chars=CSRF_ALLOWED_CHARS)
59def _mask_cipher_secret(secret):
60 """
61 Given a secret (assumed to be a string of CSRF_ALLOWED_CHARS), generate a
62 token by adding a mask and applying it to the secret.
63 """
64 mask = _get_new_csrf_string()
65 chars = CSRF_ALLOWED_CHARS
66 pairs = zip((chars.index(x) for x in secret), (chars.index(x) for x in mask))
67 cipher = "".join(chars[(x + y) % len(chars)] for x, y in pairs)
68 return mask + cipher
71def _unmask_cipher_token(token):
72 """
73 Given a token (assumed to be a string of CSRF_ALLOWED_CHARS, of length
74 CSRF_TOKEN_LENGTH, and that its first half is a mask), use it to decrypt
75 the second half to produce the original secret.
76 """
77 mask = token[:CSRF_SECRET_LENGTH]
78 token = token[CSRF_SECRET_LENGTH:]
79 chars = CSRF_ALLOWED_CHARS
80 pairs = zip((chars.index(x) for x in token), (chars.index(x) for x in mask))
81 return "".join(chars[x - y] for x, y in pairs) # Note negative values are ok
84def _add_new_csrf_cookie(request):
85 """Generate a new random CSRF_COOKIE value, and add it to request.META."""
86 csrf_secret = _get_new_csrf_string()
87 request.META.update(
88 {
89 "CSRF_COOKIE": _mask_cipher_secret(csrf_secret),
90 "CSRF_COOKIE_NEEDS_UPDATE": True,
91 }
92 )
93 return csrf_secret
96def get_token(request):
97 """
98 Return the CSRF token required for a POST form. The token is an
99 alphanumeric value. A new token is created if one is not already set.
101 A side effect of calling this function is to make the csrf_protect
102 decorator and the CsrfViewMiddleware add a CSRF cookie and a 'Vary: Cookie'
103 header to the outgoing response. For this reason, you may need to use this
104 function lazily, as is done by the csrf context processor.
105 """
106 if "CSRF_COOKIE" in request.META:
107 csrf_secret = _unmask_cipher_token(request.META["CSRF_COOKIE"])
108 # Since the cookie is being used, flag to send the cookie in
109 # process_response() (even if the client already has it) in order to
110 # renew the expiry timer.
111 request.META["CSRF_COOKIE_NEEDS_UPDATE"] = True
112 else:
113 csrf_secret = _add_new_csrf_cookie(request)
114 return _mask_cipher_secret(csrf_secret)
117def rotate_token(request):
118 """
119 Change the CSRF token in use for a request - should be done on login
120 for security purposes.
121 """
122 _add_new_csrf_cookie(request)
125class InvalidTokenFormat(Exception):
126 def __init__(self, reason):
127 self.reason = reason
130def _sanitize_token(token):
131 if len(token) not in (CSRF_TOKEN_LENGTH, CSRF_SECRET_LENGTH):
132 raise InvalidTokenFormat(REASON_INCORRECT_LENGTH)
133 # Make sure all characters are in CSRF_ALLOWED_CHARS.
134 if invalid_token_chars_re.search(token):
135 raise InvalidTokenFormat(REASON_INVALID_CHARACTERS)
136 if len(token) == CSRF_SECRET_LENGTH:
137 # Older Django versions set cookies to values of CSRF_SECRET_LENGTH
138 # alphanumeric characters. For backwards compatibility, accept
139 # such values as unmasked secrets.
140 # It's easier to mask here and be consistent later, rather than add
141 # different code paths in the checks, although that might be a tad more
142 # efficient.
143 return _mask_cipher_secret(token)
144 return token
147def _does_token_match(request_csrf_token, csrf_token):
148 # Assume both arguments are sanitized -- that is, strings of
149 # length CSRF_TOKEN_LENGTH, all CSRF_ALLOWED_CHARS.
150 return constant_time_compare(
151 _unmask_cipher_token(request_csrf_token),
152 _unmask_cipher_token(csrf_token),
153 )
156class RejectRequest(Exception):
157 def __init__(self, reason):
158 self.reason = reason
161class CsrfViewMiddleware(MiddlewareMixin):
162 """
163 Require a present and correct csrfmiddlewaretoken for POST requests that
164 have a CSRF cookie, and set an outgoing CSRF cookie.
166 This middleware should be used in conjunction with the {% csrf_token %}
167 template tag.
168 """
170 @cached_property
171 def csrf_trusted_origins_hosts(self):
172 return [
173 urlparse(origin).netloc.lstrip("*")
174 for origin in settings.CSRF_TRUSTED_ORIGINS
175 ]
177 @cached_property
178 def allowed_origins_exact(self):
179 return {origin for origin in settings.CSRF_TRUSTED_ORIGINS if "*" not in origin}
181 @cached_property
182 def allowed_origin_subdomains(self):
183 """
184 A mapping of allowed schemes to list of allowed netlocs, where all
185 subdomains of the netloc are allowed.
186 """
187 allowed_origin_subdomains = defaultdict(list)
188 for parsed in (
189 urlparse(origin)
190 for origin in settings.CSRF_TRUSTED_ORIGINS
191 if "*" in origin
192 ):
193 allowed_origin_subdomains[parsed.scheme].append(parsed.netloc.lstrip("*"))
194 return allowed_origin_subdomains
196 # The _accept and _reject methods currently only exist for the sake of the
197 # requires_csrf_token decorator.
198 def _accept(self, request):
199 # Avoid checking the request twice by adding a custom attribute to
200 # request. This will be relevant when both decorator and middleware
201 # are used.
202 request.csrf_processing_done = True
203 return None
205 def _reject(self, request, reason):
206 response = _get_failure_view()(request, reason=reason)
207 log_response(
208 "Forbidden (%s): %s",
209 reason,
210 request.path,
211 response=response,
212 request=request,
213 logger=logger,
214 )
215 return response
217 def _get_token(self, request):
218 if settings.CSRF_USE_SESSIONS: 218 ↛ 219line 218 didn't jump to line 219, because the condition on line 218 was never true
219 try:
220 return request.session.get(CSRF_SESSION_KEY)
221 except AttributeError:
222 raise ImproperlyConfigured(
223 "CSRF_USE_SESSIONS is enabled, but request.session is not "
224 "set. SessionMiddleware must appear before CsrfViewMiddleware "
225 "in MIDDLEWARE."
226 )
227 else:
228 try:
229 cookie_token = request.COOKIES[settings.CSRF_COOKIE_NAME]
230 except KeyError:
231 return None
233 # This can raise InvalidTokenFormat.
234 csrf_token = _sanitize_token(cookie_token)
236 if csrf_token != cookie_token:
237 # Then the cookie token had length CSRF_SECRET_LENGTH, so flag
238 # to replace it with the masked version.
239 request.META["CSRF_COOKIE_NEEDS_UPDATE"] = True
240 return csrf_token
242 def _set_csrf_cookie(self, request, response):
243 if settings.CSRF_USE_SESSIONS:
244 if request.session.get(CSRF_SESSION_KEY) != request.META["CSRF_COOKIE"]:
245 request.session[CSRF_SESSION_KEY] = request.META["CSRF_COOKIE"]
246 else:
247 response.set_cookie(
248 settings.CSRF_COOKIE_NAME,
249 request.META["CSRF_COOKIE"],
250 max_age=settings.CSRF_COOKIE_AGE,
251 domain=settings.CSRF_COOKIE_DOMAIN,
252 path=settings.CSRF_COOKIE_PATH,
253 secure=settings.CSRF_COOKIE_SECURE,
254 httponly=settings.CSRF_COOKIE_HTTPONLY,
255 samesite=settings.CSRF_COOKIE_SAMESITE,
256 )
257 # Set the Vary header since content varies with the CSRF cookie.
258 patch_vary_headers(response, ("Cookie",))
260 def _origin_verified(self, request):
261 request_origin = request.META["HTTP_ORIGIN"]
262 try:
263 good_host = request.get_host()
264 except DisallowedHost:
265 pass
266 else:
267 good_origin = "%s://%s" % (
268 "https" if request.is_secure() else "http",
269 good_host,
270 )
271 if request_origin == good_origin:
272 return True
273 if request_origin in self.allowed_origins_exact:
274 return True
275 try:
276 parsed_origin = urlparse(request_origin)
277 except ValueError:
278 return False
279 request_scheme = parsed_origin.scheme
280 request_netloc = parsed_origin.netloc
281 return any(
282 is_same_domain(request_netloc, host)
283 for host in self.allowed_origin_subdomains.get(request_scheme, ())
284 )
286 def _check_referer(self, request):
287 referer = request.META.get("HTTP_REFERER")
288 if referer is None:
289 raise RejectRequest(REASON_NO_REFERER)
291 try:
292 referer = urlparse(referer)
293 except ValueError:
294 raise RejectRequest(REASON_MALFORMED_REFERER)
296 # Make sure we have a valid URL for Referer.
297 if "" in (referer.scheme, referer.netloc):
298 raise RejectRequest(REASON_MALFORMED_REFERER)
300 # Ensure that our Referer is also secure.
301 if referer.scheme != "https":
302 raise RejectRequest(REASON_INSECURE_REFERER)
304 if any(
305 is_same_domain(referer.netloc, host)
306 for host in self.csrf_trusted_origins_hosts
307 ):
308 return
309 # Allow matching the configured cookie domain.
310 good_referer = (
311 settings.SESSION_COOKIE_DOMAIN
312 if settings.CSRF_USE_SESSIONS
313 else settings.CSRF_COOKIE_DOMAIN
314 )
315 if good_referer is None:
316 # If no cookie domain is configured, allow matching the current
317 # host:port exactly if it's permitted by ALLOWED_HOSTS.
318 try:
319 # request.get_host() includes the port.
320 good_referer = request.get_host()
321 except DisallowedHost:
322 raise RejectRequest(REASON_BAD_REFERER % referer.geturl())
323 else:
324 server_port = request.get_port()
325 if server_port not in ("443", "80"):
326 good_referer = "%s:%s" % (good_referer, server_port)
328 if not is_same_domain(referer.netloc, good_referer):
329 raise RejectRequest(REASON_BAD_REFERER % referer.geturl())
331 def _bad_token_message(self, reason, token_source):
332 if token_source != "POST":
333 # Assume it is a settings.CSRF_HEADER_NAME value.
334 header_name = HttpHeaders.parse_header_name(token_source)
335 token_source = f"the {header_name!r} HTTP header"
336 return f"CSRF token from {token_source} {reason}."
338 def _check_token(self, request):
339 # Access csrf_token via self._get_token() as rotate_token() may have
340 # been called by an authentication middleware during the
341 # process_request() phase.
342 try:
343 csrf_token = self._get_token(request)
344 except InvalidTokenFormat as exc:
345 raise RejectRequest(f"CSRF cookie {exc.reason}.")
347 if csrf_token is None:
348 # No CSRF cookie. For POST requests, we insist on a CSRF cookie,
349 # and in this way we can avoid all CSRF attacks, including login
350 # CSRF.
351 raise RejectRequest(REASON_NO_CSRF_COOKIE)
353 # Check non-cookie token for match.
354 request_csrf_token = ""
355 if request.method == "POST":
356 try:
357 request_csrf_token = request.POST.get("csrfmiddlewaretoken", "")
358 except UnreadablePostError:
359 # Handle a broken connection before we've completed reading the
360 # POST data. process_view shouldn't raise any exceptions, so
361 # we'll ignore and serve the user a 403 (assuming they're still
362 # listening, which they probably aren't because of the error).
363 pass
365 if request_csrf_token == "":
366 # Fall back to X-CSRFToken, to make things easier for AJAX, and
367 # possible for PUT/DELETE.
368 try:
369 request_csrf_token = request.META[settings.CSRF_HEADER_NAME]
370 except KeyError:
371 raise RejectRequest(REASON_CSRF_TOKEN_MISSING)
372 token_source = settings.CSRF_HEADER_NAME
373 else:
374 token_source = "POST"
376 try:
377 request_csrf_token = _sanitize_token(request_csrf_token)
378 except InvalidTokenFormat as exc:
379 reason = self._bad_token_message(exc.reason, token_source)
380 raise RejectRequest(reason)
382 if not _does_token_match(request_csrf_token, csrf_token):
383 reason = self._bad_token_message("incorrect", token_source)
384 raise RejectRequest(reason)
386 def process_request(self, request):
387 try:
388 csrf_token = self._get_token(request)
389 except InvalidTokenFormat:
390 _add_new_csrf_cookie(request)
391 else:
392 if csrf_token is not None: 392 ↛ 394line 392 didn't jump to line 394, because the condition on line 392 was never true
393 # Use same token next time.
394 request.META["CSRF_COOKIE"] = csrf_token
396 def process_view(self, request, callback, callback_args, callback_kwargs):
397 if getattr(request, "csrf_processing_done", False): 397 ↛ 398line 397 didn't jump to line 398, because the condition on line 397 was never true
398 return None
400 # Wait until request.META["CSRF_COOKIE"] has been manipulated before
401 # bailing out, so that get_token still works
402 if getattr(callback, "csrf_exempt", False): 402 ↛ 406line 402 didn't jump to line 406, because the condition on line 402 was never false
403 return None
405 # Assume that anything not defined as 'safe' by RFC7231 needs protection
406 if request.method in ("GET", "HEAD", "OPTIONS", "TRACE"):
407 return self._accept(request)
409 if getattr(request, "_dont_enforce_csrf_checks", False):
410 # Mechanism to turn off CSRF checks for test suite. It comes after
411 # the creation of CSRF cookies, so that everything else continues
412 # to work exactly the same (e.g. cookies are sent, etc.), but
413 # before any branches that call the _reject method.
414 return self._accept(request)
416 # Reject the request if the Origin header doesn't match an allowed
417 # value.
418 if "HTTP_ORIGIN" in request.META:
419 if not self._origin_verified(request):
420 return self._reject(
421 request, REASON_BAD_ORIGIN % request.META["HTTP_ORIGIN"]
422 )
423 elif request.is_secure():
424 # If the Origin header wasn't provided, reject HTTPS requests if
425 # the Referer header doesn't match an allowed value.
426 #
427 # Suppose user visits http://example.com/
428 # An active network attacker (man-in-the-middle, MITM) sends a
429 # POST form that targets https://example.com/detonate-bomb/ and
430 # submits it via JavaScript.
431 #
432 # The attacker will need to provide a CSRF cookie and token, but
433 # that's no problem for a MITM and the session-independent secret
434 # we're using. So the MITM can circumvent the CSRF protection. This
435 # is true for any HTTP connection, but anyone using HTTPS expects
436 # better! For this reason, for https://example.com/ we need
437 # additional protection that treats http://example.com/ as
438 # completely untrusted. Under HTTPS, Barth et al. found that the
439 # Referer header is missing for same-domain requests in only about
440 # 0.2% of cases or less, so we can use strict Referer checking.
441 try:
442 self._check_referer(request)
443 except RejectRequest as exc:
444 return self._reject(request, exc.reason)
446 try:
447 self._check_token(request)
448 except RejectRequest as exc:
449 return self._reject(request, exc.reason)
451 return self._accept(request)
453 def process_response(self, request, response):
454 if request.META.get("CSRF_COOKIE_NEEDS_UPDATE"): 454 ↛ 455line 454 didn't jump to line 455, because the condition on line 454 was never true
455 self._set_csrf_cookie(request, response)
456 # Unset the flag to prevent _set_csrf_cookie() from being
457 # unnecessarily called again in process_response() by other
458 # instances of CsrfViewMiddleware. This can happen e.g. when both a
459 # decorator and middleware are used. However,
460 # CSRF_COOKIE_NEEDS_UPDATE is still respected in subsequent calls
461 # e.g. in case rotate_token() is called in process_response() later
462 # by custom middleware but before those subsequent calls.
463 request.META["CSRF_COOKIE_NEEDS_UPDATE"] = False
465 return response