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

1""" 

2Cross Site Request Forgery Middleware. 

3 

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 

11 

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 

24 

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]") 

28 

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" 

43 

44CSRF_SECRET_LENGTH = 32 

45CSRF_TOKEN_LENGTH = 2 * CSRF_SECRET_LENGTH 

46CSRF_ALLOWED_CHARS = string.ascii_letters + string.digits 

47CSRF_SESSION_KEY = "_csrftoken" 

48 

49 

50def _get_failure_view(): 

51 """Return the view to be used for CSRF rejections.""" 

52 return get_callable(settings.CSRF_FAILURE_VIEW) 

53 

54 

55def _get_new_csrf_string(): 

56 return get_random_string(CSRF_SECRET_LENGTH, allowed_chars=CSRF_ALLOWED_CHARS) 

57 

58 

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 

69 

70 

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 

82 

83 

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 

94 

95 

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. 

100 

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) 

115 

116 

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) 

123 

124 

125class InvalidTokenFormat(Exception): 

126 def __init__(self, reason): 

127 self.reason = reason 

128 

129 

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 

145 

146 

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 ) 

154 

155 

156class RejectRequest(Exception): 

157 def __init__(self, reason): 

158 self.reason = reason 

159 

160 

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. 

165 

166 This middleware should be used in conjunction with the {% csrf_token %} 

167 template tag. 

168 """ 

169 

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 ] 

176 

177 @cached_property 

178 def allowed_origins_exact(self): 

179 return {origin for origin in settings.CSRF_TRUSTED_ORIGINS if "*" not in origin} 

180 

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 

195 

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 

204 

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 

216 

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 

232 

233 # This can raise InvalidTokenFormat. 

234 csrf_token = _sanitize_token(cookie_token) 

235 

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 

241 

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",)) 

259 

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 ) 

285 

286 def _check_referer(self, request): 

287 referer = request.META.get("HTTP_REFERER") 

288 if referer is None: 

289 raise RejectRequest(REASON_NO_REFERER) 

290 

291 try: 

292 referer = urlparse(referer) 

293 except ValueError: 

294 raise RejectRequest(REASON_MALFORMED_REFERER) 

295 

296 # Make sure we have a valid URL for Referer. 

297 if "" in (referer.scheme, referer.netloc): 

298 raise RejectRequest(REASON_MALFORMED_REFERER) 

299 

300 # Ensure that our Referer is also secure. 

301 if referer.scheme != "https": 

302 raise RejectRequest(REASON_INSECURE_REFERER) 

303 

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) 

327 

328 if not is_same_domain(referer.netloc, good_referer): 

329 raise RejectRequest(REASON_BAD_REFERER % referer.geturl()) 

330 

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}." 

337 

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}.") 

346 

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) 

352 

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 

364 

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" 

375 

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) 

381 

382 if not _does_token_match(request_csrf_token, csrf_token): 

383 reason = self._bad_token_message("incorrect", token_source) 

384 raise RejectRequest(reason) 

385 

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 

395 

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 

399 

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 

404 

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) 

408 

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) 

415 

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) 

445 

446 try: 

447 self._check_token(request) 

448 except RejectRequest as exc: 

449 return self._reject(request, exc.reason) 

450 

451 return self._accept(request) 

452 

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 

464 

465 return response