1import functools
2import inspect
3import itertools
4import re
5import sys
6import types
7import warnings
8from pathlib import Path
9
10from django.conf import settings
11from django.http import Http404, HttpResponse, HttpResponseNotFound
12from django.template import Context, Engine, TemplateDoesNotExist
13from django.template.defaultfilters import pprint
14from django.urls import resolve
15from django.utils import timezone
16from django.utils.datastructures import MultiValueDict
17from django.utils.encoding import force_str
18from django.utils.module_loading import import_string
19from django.utils.regex_helper import _lazy_re_compile
20from django.utils.version import PY311, get_docs_version
21from django.views.decorators.debug import coroutine_functions_to_sensitive_variables
22
23# Minimal Django templates engine to render the error templates
24# regardless of the project's TEMPLATES setting. Templates are
25# read directly from the filesystem so that the error handler
26# works even if the template loader is broken.
27DEBUG_ENGINE = Engine(
28 debug=True,
29 libraries={"i18n": "django.templatetags.i18n"},
30)
31
32
33def builtin_template_path(name):
34 """
35 Return a path to a builtin template.
36
37 Avoid calling this function at the module level or in a class-definition
38 because __file__ may not exist, e.g. in frozen environments.
39 """
40 return Path(__file__).parent / "templates" / name
41
42
43class ExceptionCycleWarning(UserWarning):
44 pass
45
46
47class CallableSettingWrapper:
48 """
49 Object to wrap callable appearing in settings.
50 * Not to call in the debug page (#21345).
51 * Not to break the debug page if the callable forbidding to set attributes
52 (#23070).
53 """
54
55 def __init__(self, callable_setting):
56 self._wrapped = callable_setting
57
58 def __repr__(self):
59 return repr(self._wrapped)
60
61
62def technical_500_response(request, exc_type, exc_value, tb, status_code=500):
63 """
64 Create a technical server error response. The last three arguments are
65 the values returned from sys.exc_info() and friends.
66 """
67 reporter = get_exception_reporter_class(request)(request, exc_type, exc_value, tb)
68 if request.accepts("text/html"):
69 html = reporter.get_traceback_html()
70 return HttpResponse(html, status=status_code)
71 else:
72 text = reporter.get_traceback_text()
73 return HttpResponse(
74 text, status=status_code, content_type="text/plain; charset=utf-8"
75 )
76
77
78@functools.lru_cache
79def get_default_exception_reporter_filter():
80 # Instantiate the default filter for the first time and cache it.
81 return import_string(settings.DEFAULT_EXCEPTION_REPORTER_FILTER)()
82
83
84def get_exception_reporter_filter(request):
85 default_filter = get_default_exception_reporter_filter()
86 return getattr(request, "exception_reporter_filter", default_filter)
87
88
89def get_exception_reporter_class(request):
90 default_exception_reporter_class = import_string(
91 settings.DEFAULT_EXCEPTION_REPORTER
92 )
93 return getattr(
94 request, "exception_reporter_class", default_exception_reporter_class
95 )
96
97
98def get_caller(request):
99 resolver_match = request.resolver_match
100 if resolver_match is None:
101 try:
102 resolver_match = resolve(request.path)
103 except Http404:
104 pass
105 return "" if resolver_match is None else resolver_match._func_path
106
107
108class SafeExceptionReporterFilter:
109 """
110 Use annotations made by the sensitive_post_parameters and
111 sensitive_variables decorators to filter out sensitive information.
112 """
113
114 cleansed_substitute = "********************"
115 hidden_settings = _lazy_re_compile(
116 "API|AUTH|TOKEN|KEY|SECRET|PASS|SIGNATURE|HTTP_COOKIE", flags=re.I
117 )
118
119 def cleanse_setting(self, key, value):
120 """
121 Cleanse an individual setting key/value of sensitive content. If the
122 value is a dictionary, recursively cleanse the keys in that dictionary.
123 """
124 if key == settings.SESSION_COOKIE_NAME:
125 is_sensitive = True
126 else:
127 try:
128 is_sensitive = self.hidden_settings.search(key)
129 except TypeError:
130 is_sensitive = False
131
132 if is_sensitive:
133 cleansed = self.cleansed_substitute
134 elif isinstance(value, dict):
135 cleansed = {k: self.cleanse_setting(k, v) for k, v in value.items()}
136 elif isinstance(value, list):
137 cleansed = [self.cleanse_setting("", v) for v in value]
138 elif isinstance(value, tuple):
139 cleansed = tuple([self.cleanse_setting("", v) for v in value])
140 else:
141 cleansed = value
142
143 if callable(cleansed):
144 cleansed = CallableSettingWrapper(cleansed)
145
146 return cleansed
147
148 def get_safe_settings(self):
149 """
150 Return a dictionary of the settings module with values of sensitive
151 settings replaced with stars (*********).
152 """
153 settings_dict = {}
154 for k in dir(settings):
155 if k.isupper():
156 settings_dict[k] = self.cleanse_setting(k, getattr(settings, k))
157 return settings_dict
158
159 def get_safe_request_meta(self, request):
160 """
161 Return a dictionary of request.META with sensitive values redacted.
162 """
163 if not hasattr(request, "META"):
164 return {}
165 return {k: self.cleanse_setting(k, v) for k, v in request.META.items()}
166
167 def get_safe_cookies(self, request):
168 """
169 Return a dictionary of request.COOKIES with sensitive values redacted.
170 """
171 if not hasattr(request, "COOKIES"):
172 return {}
173 return {k: self.cleanse_setting(k, v) for k, v in request.COOKIES.items()}
174
175 def is_active(self, request):
176 """
177 This filter is to add safety in production environments (i.e. DEBUG
178 is False). If DEBUG is True then your site is not safe anyway.
179 This hook is provided as a convenience to easily activate or
180 deactivate the filter on a per request basis.
181 """
182 return settings.DEBUG is False
183
184 def get_cleansed_multivaluedict(self, request, multivaluedict):
185 """
186 Replace the keys in a MultiValueDict marked as sensitive with stars.
187 This mitigates leaking sensitive POST parameters if something like
188 request.POST['nonexistent_key'] throws an exception (#21098).
189 """
190 sensitive_post_parameters = getattr(request, "sensitive_post_parameters", [])
191 if self.is_active(request) and sensitive_post_parameters:
192 multivaluedict = multivaluedict.copy()
193 for param in sensitive_post_parameters:
194 if param in multivaluedict:
195 multivaluedict[param] = self.cleansed_substitute
196 return multivaluedict
197
198 def get_post_parameters(self, request):
199 """
200 Replace the values of POST parameters marked as sensitive with
201 stars (*********).
202 """
203 if request is None:
204 return {}
205 else:
206 sensitive_post_parameters = getattr(
207 request, "sensitive_post_parameters", []
208 )
209 if self.is_active(request) and sensitive_post_parameters:
210 cleansed = request.POST.copy()
211 if sensitive_post_parameters == "__ALL__":
212 # Cleanse all parameters.
213 for k in cleansed:
214 cleansed[k] = self.cleansed_substitute
215 return cleansed
216 else:
217 # Cleanse only the specified parameters.
218 for param in sensitive_post_parameters:
219 if param in cleansed:
220 cleansed[param] = self.cleansed_substitute
221 return cleansed
222 else:
223 return request.POST
224
225 def cleanse_special_types(self, request, value):
226 try:
227 # If value is lazy or a complex object of another kind, this check
228 # might raise an exception. isinstance checks that lazy
229 # MultiValueDicts will have a return value.
230 is_multivalue_dict = isinstance(value, MultiValueDict)
231 except Exception as e:
232 return "{!r} while evaluating {!r}".format(e, value)
233
234 if is_multivalue_dict:
235 # Cleanse MultiValueDicts (request.POST is the one we usually care about)
236 value = self.get_cleansed_multivaluedict(request, value)
237 return value
238
239 def get_traceback_frame_variables(self, request, tb_frame):
240 """
241 Replace the values of variables marked as sensitive with
242 stars (*********).
243 """
244 sensitive_variables = None
245
246 # Coroutines don't have a proper `f_back` so they need to be inspected
247 # separately. Handle this by stashing the registered sensitive
248 # variables in a global dict indexed by `hash(file_path:line_number)`.
249 if (
250 tb_frame.f_code.co_flags & inspect.CO_COROUTINE != 0
251 and tb_frame.f_code.co_name != "sensitive_variables_wrapper"
252 ):
253 key = hash(
254 f"{tb_frame.f_code.co_filename}:{tb_frame.f_code.co_firstlineno}"
255 )
256 sensitive_variables = coroutine_functions_to_sensitive_variables.get(
257 key, None
258 )
259
260 if sensitive_variables is None:
261 # Loop through the frame's callers to see if the
262 # sensitive_variables decorator was used.
263 current_frame = tb_frame
264 while current_frame is not None:
265 if (
266 current_frame.f_code.co_name == "sensitive_variables_wrapper"
267 and "sensitive_variables_wrapper" in current_frame.f_locals
268 ):
269 # The sensitive_variables decorator was used, so take note
270 # of the sensitive variables' names.
271 wrapper = current_frame.f_locals["sensitive_variables_wrapper"]
272 sensitive_variables = getattr(wrapper, "sensitive_variables", None)
273 break
274 current_frame = current_frame.f_back
275
276 cleansed = {}
277 if self.is_active(request) and sensitive_variables:
278 if sensitive_variables == "__ALL__":
279 # Cleanse all variables
280 for name in tb_frame.f_locals:
281 cleansed[name] = self.cleansed_substitute
282 else:
283 # Cleanse specified variables
284 for name, value in tb_frame.f_locals.items():
285 if name in sensitive_variables:
286 value = self.cleansed_substitute
287 else:
288 value = self.cleanse_special_types(request, value)
289 cleansed[name] = value
290 else:
291 # Potentially cleanse the request and any MultiValueDicts if they
292 # are one of the frame variables.
293 for name, value in tb_frame.f_locals.items():
294 cleansed[name] = self.cleanse_special_types(request, value)
295
296 if (
297 tb_frame.f_code.co_name == "sensitive_variables_wrapper"
298 and "sensitive_variables_wrapper" in tb_frame.f_locals
299 ):
300 # For good measure, obfuscate the decorated function's arguments in
301 # the sensitive_variables decorator's frame, in case the variables
302 # associated with those arguments were meant to be obfuscated from
303 # the decorated function's frame.
304 cleansed["func_args"] = self.cleansed_substitute
305 cleansed["func_kwargs"] = self.cleansed_substitute
306
307 return cleansed.items()
308
309
310class ExceptionReporter:
311 """Organize and coordinate reporting on exceptions."""
312
313 @property
314 def html_template_path(self):
315 return builtin_template_path("technical_500.html")
316
317 @property
318 def text_template_path(self):
319 return builtin_template_path("technical_500.txt")
320
321 def __init__(self, request, exc_type, exc_value, tb, is_email=False):
322 self.request = request
323 self.filter = get_exception_reporter_filter(self.request)
324 self.exc_type = exc_type
325 self.exc_value = exc_value
326 self.tb = tb
327 self.is_email = is_email
328
329 self.template_info = getattr(self.exc_value, "template_debug", None)
330 self.template_does_not_exist = False
331 self.postmortem = None
332
333 def _get_raw_insecure_uri(self):
334 """
335 Return an absolute URI from variables available in this request. Skip
336 allowed hosts protection, so may return insecure URI.
337 """
338 return "{scheme}://{host}{path}".format(
339 scheme=self.request.scheme,
340 host=self.request._get_raw_host(),
341 path=self.request.get_full_path(),
342 )
343
344 def get_traceback_data(self):
345 """Return a dictionary containing traceback information."""
346 if self.exc_type and issubclass(self.exc_type, TemplateDoesNotExist):
347 self.template_does_not_exist = True
348 self.postmortem = self.exc_value.chain or [self.exc_value]
349
350 frames = self.get_traceback_frames()
351 for i, frame in enumerate(frames):
352 if "vars" in frame:
353 frame_vars = []
354 for k, v in frame["vars"]:
355 v = pprint(v)
356 # Trim large blobs of data
357 if len(v) > 4096:
358 v = "%s… <trimmed %d bytes string>" % (v[0:4096], len(v))
359 frame_vars.append((k, v))
360 frame["vars"] = frame_vars
361 frames[i] = frame
362
363 unicode_hint = ""
364 if self.exc_type and issubclass(self.exc_type, UnicodeError):
365 start = getattr(self.exc_value, "start", None)
366 end = getattr(self.exc_value, "end", None)
367 if start is not None and end is not None:
368 unicode_str = self.exc_value.args[1]
369 unicode_hint = force_str(
370 unicode_str[max(start - 5, 0) : min(end + 5, len(unicode_str))],
371 "ascii",
372 errors="replace",
373 )
374 from django import get_version
375
376 if self.request is None:
377 user_str = None
378 else:
379 try:
380 user_str = str(self.request.user)
381 except Exception:
382 # request.user may raise OperationalError if the database is
383 # unavailable, for example.
384 user_str = "[unable to retrieve the current user]"
385
386 c = {
387 "is_email": self.is_email,
388 "unicode_hint": unicode_hint,
389 "frames": frames,
390 "request": self.request,
391 "request_meta": self.filter.get_safe_request_meta(self.request),
392 "request_COOKIES_items": self.filter.get_safe_cookies(self.request).items(),
393 "user_str": user_str,
394 "filtered_POST_items": list(
395 self.filter.get_post_parameters(self.request).items()
396 ),
397 "settings": self.filter.get_safe_settings(),
398 "sys_executable": sys.executable,
399 "sys_version_info": "%d.%d.%d" % sys.version_info[0:3],
400 "server_time": timezone.now(),
401 "django_version_info": get_version(),
402 "sys_path": sys.path,
403 "template_info": self.template_info,
404 "template_does_not_exist": self.template_does_not_exist,
405 "postmortem": self.postmortem,
406 }
407 if self.request is not None:
408 c["request_GET_items"] = self.request.GET.items()
409 c["request_FILES_items"] = self.request.FILES.items()
410 c["request_insecure_uri"] = self._get_raw_insecure_uri()
411 c["raising_view_name"] = get_caller(self.request)
412
413 # Check whether exception info is available
414 if self.exc_type:
415 c["exception_type"] = self.exc_type.__name__
416 if self.exc_value:
417 c["exception_value"] = str(self.exc_value)
418 if exc_notes := getattr(self.exc_value, "__notes__", None):
419 c["exception_notes"] = "\n" + "\n".join(exc_notes)
420 if frames:
421 c["lastframe"] = frames[-1]
422 return c
423
424 def get_traceback_html(self):
425 """Return HTML version of debug 500 HTTP error page."""
426 with self.html_template_path.open(encoding="utf-8") as fh:
427 t = DEBUG_ENGINE.from_string(fh.read())
428 c = Context(self.get_traceback_data(), use_l10n=False)
429 return t.render(c)
430
431 def get_traceback_text(self):
432 """Return plain text version of debug 500 HTTP error page."""
433 with self.text_template_path.open(encoding="utf-8") as fh:
434 t = DEBUG_ENGINE.from_string(fh.read())
435 c = Context(self.get_traceback_data(), autoescape=False, use_l10n=False)
436 return t.render(c)
437
438 def _get_source(self, filename, loader, module_name):
439 source = None
440 if hasattr(loader, "get_source"):
441 try:
442 source = loader.get_source(module_name)
443 except ImportError:
444 pass
445 if source is not None:
446 source = source.splitlines()
447 if source is None:
448 try:
449 with open(filename, "rb") as fp:
450 source = fp.read().splitlines()
451 except OSError:
452 pass
453 return source
454
455 def _get_lines_from_file(
456 self, filename, lineno, context_lines, loader=None, module_name=None
457 ):
458 """
459 Return context_lines before and after lineno from file.
460 Return (pre_context_lineno, pre_context, context_line, post_context).
461 """
462 source = self._get_source(filename, loader, module_name)
463 if source is None:
464 return None, [], None, []
465
466 # If we just read the source from a file, or if the loader did not
467 # apply tokenize.detect_encoding to decode the source into a
468 # string, then we should do that ourselves.
469 if isinstance(source[0], bytes):
470 encoding = "ascii"
471 for line in source[:2]:
472 # File coding may be specified. Match pattern from PEP-263
473 # (https://www.python.org/dev/peps/pep-0263/)
474 match = re.search(rb"coding[:=]\s*([-\w.]+)", line)
475 if match:
476 encoding = match[1].decode("ascii")
477 break
478 source = [str(sline, encoding, "replace") for sline in source]
479
480 lower_bound = max(0, lineno - context_lines)
481 upper_bound = lineno + context_lines
482
483 try:
484 pre_context = source[lower_bound:lineno]
485 context_line = source[lineno]
486 post_context = source[lineno + 1 : upper_bound]
487 except IndexError:
488 return None, [], None, []
489 return lower_bound, pre_context, context_line, post_context
490
491 def _get_explicit_or_implicit_cause(self, exc_value):
492 explicit = getattr(exc_value, "__cause__", None)
493 suppress_context = getattr(exc_value, "__suppress_context__", None)
494 implicit = getattr(exc_value, "__context__", None)
495 return explicit or (None if suppress_context else implicit)
496
497 def get_traceback_frames(self):
498 # Get the exception and all its causes
499 exceptions = []
500 exc_value = self.exc_value
501 while exc_value:
502 exceptions.append(exc_value)
503 exc_value = self._get_explicit_or_implicit_cause(exc_value)
504 if exc_value in exceptions:
505 warnings.warn(
506 "Cycle in the exception chain detected: exception '%s' "
507 "encountered again." % exc_value,
508 ExceptionCycleWarning,
509 )
510 # Avoid infinite loop if there's a cyclic reference (#29393).
511 break
512
513 frames = []
514 # No exceptions were supplied to ExceptionReporter
515 if not exceptions:
516 return frames
517
518 # In case there's just one exception, take the traceback from self.tb
519 exc_value = exceptions.pop()
520 tb = self.tb if not exceptions else exc_value.__traceback__
521 while True:
522 frames.extend(self.get_exception_traceback_frames(exc_value, tb))
523 try:
524 exc_value = exceptions.pop()
525 except IndexError:
526 break
527 tb = exc_value.__traceback__
528 return frames
529
530 def get_exception_traceback_frames(self, exc_value, tb):
531 exc_cause = self._get_explicit_or_implicit_cause(exc_value)
532 exc_cause_explicit = getattr(exc_value, "__cause__", True)
533 if tb is None:
534 yield {
535 "exc_cause": exc_cause,
536 "exc_cause_explicit": exc_cause_explicit,
537 "tb": None,
538 "type": "user",
539 }
540 while tb is not None:
541 # Support for __traceback_hide__ which is used by a few libraries
542 # to hide internal frames.
543 if tb.tb_frame.f_locals.get("__traceback_hide__"):
544 tb = tb.tb_next
545 continue
546 filename = tb.tb_frame.f_code.co_filename
547 function = tb.tb_frame.f_code.co_name
548 lineno = tb.tb_lineno - 1
549 loader = tb.tb_frame.f_globals.get("__loader__")
550 module_name = tb.tb_frame.f_globals.get("__name__") or ""
551 (
552 pre_context_lineno,
553 pre_context,
554 context_line,
555 post_context,
556 ) = self._get_lines_from_file(
557 filename,
558 lineno,
559 7,
560 loader,
561 module_name,
562 )
563 if pre_context_lineno is None:
564 pre_context_lineno = lineno
565 pre_context = []
566 context_line = "<source code not available>"
567 post_context = []
568
569 colno = tb_area_colno = ""
570 if PY311:
571 _, _, start_column, end_column = next(
572 itertools.islice(
573 tb.tb_frame.f_code.co_positions(), tb.tb_lasti // 2, None
574 )
575 )
576 if start_column and end_column:
577 underline = "^" * (end_column - start_column)
578 spaces = " " * (start_column + len(str(lineno + 1)) + 2)
579 colno = f"\n{spaces}{underline}"
580 tb_area_spaces = " " * (
581 4
582 + start_column
583 - (len(context_line) - len(context_line.lstrip()))
584 )
585 tb_area_colno = f"\n{tb_area_spaces}{underline}"
586 yield {
587 "exc_cause": exc_cause,
588 "exc_cause_explicit": exc_cause_explicit,
589 "tb": tb,
590 "type": "django" if module_name.startswith("django.") else "user",
591 "filename": filename,
592 "function": function,
593 "lineno": lineno + 1,
594 "vars": self.filter.get_traceback_frame_variables(
595 self.request, tb.tb_frame
596 ),
597 "id": id(tb),
598 "pre_context": pre_context,
599 "context_line": context_line,
600 "post_context": post_context,
601 "pre_context_lineno": pre_context_lineno + 1,
602 "colno": colno,
603 "tb_area_colno": tb_area_colno,
604 }
605 tb = tb.tb_next
606
607
608def technical_404_response(request, exception):
609 """Create a technical 404 error response. `exception` is the Http404."""
610 try:
611 error_url = exception.args[0]["path"]
612 except (IndexError, TypeError, KeyError):
613 error_url = request.path_info[1:] # Trim leading slash
614
615 try:
616 tried = exception.args[0]["tried"]
617 except (IndexError, TypeError, KeyError):
618 resolved = True
619 tried = request.resolver_match.tried if request.resolver_match else None
620 else:
621 resolved = False
622 if not tried or ( # empty URLconf
623 request.path_info == "/"
624 and len(tried) == 1
625 and len(tried[0]) == 1 # default URLconf
626 and getattr(tried[0][0], "app_name", "")
627 == getattr(tried[0][0], "namespace", "")
628 == "admin"
629 ):
630 return default_urlconf(request)
631
632 urlconf = getattr(request, "urlconf", settings.ROOT_URLCONF)
633 if isinstance(urlconf, types.ModuleType):
634 urlconf = urlconf.__name__
635
636 with builtin_template_path("technical_404.html").open(encoding="utf-8") as fh:
637 t = DEBUG_ENGINE.from_string(fh.read())
638 reporter_filter = get_default_exception_reporter_filter()
639 c = Context(
640 {
641 "urlconf": urlconf,
642 "root_urlconf": settings.ROOT_URLCONF,
643 "request_path": error_url,
644 "urlpatterns": tried,
645 "resolved": resolved,
646 "reason": str(exception),
647 "request": request,
648 "settings": reporter_filter.get_safe_settings(),
649 "raising_view_name": get_caller(request),
650 }
651 )
652 return HttpResponseNotFound(t.render(c))
653
654
655def default_urlconf(request):
656 """Create an empty URLconf 404 error response."""
657 with builtin_template_path("default_urlconf.html").open(encoding="utf-8") as fh:
658 t = DEBUG_ENGINE.from_string(fh.read())
659 c = Context(
660 {
661 "version": get_docs_version(),
662 }
663 )
664
665 return HttpResponse(t.render(c))