debug.py 24 KB

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